Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

614 lines
23 KiB
TypeScript

/**
* Memory Arbiter (WS1) tests.
*
* Covers:
* - Capability registration + acquire/release refcounting.
* - Same-modelKey concurrent acquires share one in-flight load.
* - Conflicting modelKey on the same role triggers an evict-then-load swap.
* - In-flight runs delay swaps (refcount > 0 waits).
* - Pressure events: low → one eviction; critical → wipe non-text.
* - Critical pressure rejects acquire for non-text capabilities.
* - Telemetry: model_load / model_unload / memory_pressure / eviction / capability_run.
* - Vision-embedding cache: hit, miss, expiry, LRU eviction.
* - Composite/poll pressure source emit nominal at boot and respond to source updates.
* - Shutdown unloads everything.
*/
import { describe, expect, it } from "vitest";
import {
type ArbiterEvent,
MemoryArbiter,
type CapabilityRegistration,
} from "../src/services/memory-arbiter";
import {
type MemoryPressureEvent,
capacitorPressureSource,
compositePressureSource,
nodeOsPressureSource,
} from "../src/services/memory-pressure";
import { VisionEmbeddingCache } from "../src/services/vision-embedding-cache";
import { SharedResourceRegistry } from "../src/services/voice/shared-resources";
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
interface FakeBackend {
id: string;
disposed: boolean;
}
function makeFakeCapability(
overrides: Partial<CapabilityRegistration<FakeBackend, { x: string }, string>> = {},
): {
registration: CapabilityRegistration<FakeBackend, { x: string }, string>;
loaded: string[];
unloaded: string[];
loadDelayMs: { value: number };
runDelayMs: { value: number };
} {
const loaded: string[] = [];
const unloaded: string[] = [];
const loadDelayMs = { value: 0 };
const runDelayMs = { value: 0 };
const registration: CapabilityRegistration<FakeBackend, { x: string }, string> = {
capability: "text",
residentRole: "text-target",
estimatedMb: 1024,
load: async (modelKey) => {
loaded.push(modelKey);
if (loadDelayMs.value > 0) {
await new Promise<void>((r) => setTimeout(r, loadDelayMs.value));
}
return { id: modelKey, disposed: false };
},
unload: async (backend) => {
unloaded.push(backend.id);
backend.disposed = true;
},
run: async (backend, req) => {
if (runDelayMs.value > 0) {
await new Promise<void>((r) => setTimeout(r, runDelayMs.value));
}
if (backend.disposed) throw new Error(`backend ${backend.id} already disposed`);
return `${backend.id}:${req.x}`;
},
...overrides,
};
return { registration, loaded, unloaded, loadDelayMs, runDelayMs };
}
function makeArbiter(opts: { capacitorBridge?: ReturnType<typeof capacitorPressureSource> } = {}): {
arbiter: MemoryArbiter;
registry: SharedResourceRegistry;
bridge: ReturnType<typeof capacitorPressureSource>;
events: ArbiterEvent[];
} {
const registry = new SharedResourceRegistry();
const bridge = opts.capacitorBridge ?? capacitorPressureSource();
const arbiter = new MemoryArbiter({
registry,
pressureSource: bridge,
visionCache: new VisionEmbeddingCache({ config: { maxEntries: 4, defaultTtlMs: 60_000 } }),
});
const events: ArbiterEvent[] = [];
arbiter.onEvent((event) => events.push(event));
arbiter.start();
return { arbiter, registry, bridge, events };
}
// ---------------------------------------------------------------------------
// Capability registration + acquire/release
// ---------------------------------------------------------------------------
describe("MemoryArbiter — registration and acquire", () => {
it("rejects acquire when no capability is registered", async () => {
const { arbiter } = makeArbiter();
await expect(arbiter.acquire("vision-describe", "gemma-vl-4b")).rejects.toThrow(
/no capability registered/,
);
});
it("rejects duplicate registration", () => {
const { arbiter } = makeArbiter();
const a = makeFakeCapability();
const b = makeFakeCapability();
arbiter.registerCapability(a.registration);
expect(() => arbiter.registerCapability(b.registration)).toThrow(/already registered/);
});
it("loads on first acquire and reuses the handle on the second", async () => {
const { arbiter } = makeArbiter();
const cap = makeFakeCapability();
arbiter.registerCapability(cap.registration);
const h1 = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const h2 = await arbiter.acquire<FakeBackend>("text", "tier-2b");
expect(cap.loaded).toEqual(["tier-2b"]);
expect(h1.backend.id).toBe("tier-2b");
expect(h2.backend).toBe(h1.backend);
await h1.release();
await h2.release();
expect(cap.unloaded).toEqual([]); // refcount=0 alone does not unload
});
it("shares a single in-flight load across concurrent acquires", async () => {
const { arbiter } = makeArbiter();
const cap = makeFakeCapability();
cap.loadDelayMs.value = 30;
arbiter.registerCapability(cap.registration);
const [h1, h2, h3] = await Promise.all([
arbiter.acquire<FakeBackend>("text", "shared-tier"),
arbiter.acquire<FakeBackend>("text", "shared-tier"),
arbiter.acquire<FakeBackend>("text", "shared-tier"),
]);
expect(cap.loaded).toEqual(["shared-tier"]);
expect(h1.backend).toBe(h2.backend);
expect(h2.backend).toBe(h3.backend);
await h1.release();
await h2.release();
await h3.release();
});
});
// ---------------------------------------------------------------------------
// Swap behaviour: same role, different modelKey
// ---------------------------------------------------------------------------
describe("MemoryArbiter — swap on conflicting role", () => {
it("evicts the previous model when the same role is loaded for a different key", async () => {
const { arbiter, events } = makeArbiter();
const cap = makeFakeCapability();
arbiter.registerCapability(cap.registration);
const a = await arbiter.acquire<FakeBackend>("text", "tier-a");
await a.release();
const b = await arbiter.acquire<FakeBackend>("text", "tier-b");
await b.release();
expect(cap.loaded).toEqual(["tier-a", "tier-b"]);
expect(cap.unloaded).toEqual(["tier-a"]);
const evictionEvents = events.filter((e) => e.type === "eviction");
expect(evictionEvents).toHaveLength(1);
expect(evictionEvents[0]).toMatchObject({
capability: "text",
modelKey: "tier-a",
reason: "swap",
});
});
it("waits for in-flight refcount holders before swapping", async () => {
const { arbiter } = makeArbiter();
const cap = makeFakeCapability();
cap.runDelayMs.value = 25;
arbiter.registerCapability(cap.registration);
const a = await arbiter.acquire<FakeBackend>("text", "tier-a");
// Hold the handle and start a fake long-running op via run() against the
// same backend.
const longRun = (async () => {
// Simulate work that retains the handle.
await new Promise<void>((r) => setTimeout(r, 30));
await a.release();
})();
// Now request the other model. It must NOT evict tier-a until the
// longRun above releases.
const swapStart = Date.now();
const b = await arbiter.acquire<FakeBackend>("text", "tier-b");
const swapMs = Date.now() - swapStart;
expect(cap.unloaded).toEqual(["tier-a"]);
// We waited at least roughly the in-flight delay; allow scheduling slack.
expect(swapMs).toBeGreaterThanOrEqual(20);
await b.release();
await longRun;
});
it("loads two different roles concurrently without swapping", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const v = await arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
expect(text.unloaded).toEqual([]);
expect(vision.unloaded).toEqual([]);
await t.release();
await v.release();
});
});
// ---------------------------------------------------------------------------
// Pressure-driven eviction
// ---------------------------------------------------------------------------
describe("MemoryArbiter — memory pressure", () => {
it("evicts the lowest-priority resident role on `low` pressure", async () => {
const { arbiter, bridge, events } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const v = await arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
await t.release();
await v.release();
// Vision is priority 20 (cheaper) than text-target (100); pressure
// should evict vision.
bridge.dispatch("low");
// Allow the async handler to drain.
await new Promise<void>((r) => setTimeout(r, 5));
expect(vision.unloaded).toEqual(["gemma-vl-4b"]);
expect(text.unloaded).toEqual([]);
const pressureEvents = events.filter((e) => e.type === "memory_pressure");
expect(pressureEvents.at(-1)?.level).toBe("low");
});
it("evicts every non-text role on `critical` pressure", async () => {
const { arbiter, bridge } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
const embedding = makeFakeCapability({
capability: "embedding",
residentRole: "embedding",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
arbiter.registerCapability(embedding.registration);
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const v = await arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
const e = await arbiter.acquire<FakeBackend>("embedding", "embedding-small");
await t.release();
await v.release();
await e.release();
bridge.dispatch("critical");
await new Promise<void>((r) => setTimeout(r, 5));
expect(vision.unloaded).toEqual(["gemma-vl-4b"]);
expect(embedding.unloaded).toEqual(["embedding-small"]);
// text-target survives critical
expect(text.unloaded).toEqual([]);
});
it("rejects non-text acquires under critical pressure", async () => {
const { arbiter, bridge } = makeArbiter();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(vision.registration);
bridge.dispatch("critical");
await new Promise<void>((r) => setTimeout(r, 5));
await expect(
arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b"),
).rejects.toThrow(/critical/);
});
it("still allows text acquires under critical pressure", async () => {
const { arbiter, bridge } = makeArbiter();
const text = makeFakeCapability();
arbiter.registerCapability(text.registration);
bridge.dispatch("critical");
await new Promise<void>((r) => setTimeout(r, 5));
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
expect(t.backend.id).toBe("tier-2b");
await t.release();
});
it("does not evict a role with refcount > 0 under pressure", async () => {
const { arbiter, bridge } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const v = await arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
// Hold onto vision (refcount=1)
await t.release();
bridge.dispatch("critical");
await new Promise<void>((r) => setTimeout(r, 5));
expect(vision.unloaded).toEqual([]);
await v.release();
});
});
// ---------------------------------------------------------------------------
// Capability request queue (the request* methods)
// ---------------------------------------------------------------------------
describe("MemoryArbiter — request queue", () => {
it("runs sequential requests against the same loaded handle", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
arbiter.registerCapability(text.registration);
const r1 = await arbiter.requestText<{ x: string }, string>({
modelKey: "tier-2b",
payload: { x: "alpha" },
});
const r2 = await arbiter.requestText<{ x: string }, string>({
modelKey: "tier-2b",
payload: { x: "beta" },
});
expect(r1).toBe("tier-2b:alpha");
expect(r2).toBe("tier-2b:beta");
expect(text.loaded).toEqual(["tier-2b"]);
});
it("emits capability_run telemetry per request", async () => {
const { arbiter, events } = makeArbiter();
const text = makeFakeCapability();
arbiter.registerCapability(text.registration);
await arbiter.requestText({ modelKey: "tier-2b", payload: { x: "y" } });
const runs = events.filter((e) => e.type === "capability_run");
expect(runs).toHaveLength(1);
expect(runs[0]).toMatchObject({ capability: "text", modelKey: "tier-2b" });
});
it("propagates run errors back to the caller without leaking the handle", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability({
run: async () => {
throw new Error("boom");
},
});
arbiter.registerCapability(text.registration);
await expect(
arbiter.requestText({ modelKey: "tier-2b", payload: { x: "y" } }),
).rejects.toThrow(/boom/);
// Despite the error the next request must still run cleanly: no
// dangling refcount, no permanent eviction.
const text2 = makeFakeCapability({
capability: "embedding",
residentRole: "embedding",
});
arbiter.registerCapability(text2.registration);
const out = await arbiter.requestEmbedding<{ x: string }, string>({
modelKey: "embed-1",
payload: { x: "z" },
});
expect(out).toBe("embed-1:z");
});
});
// ---------------------------------------------------------------------------
// Vision-embedding cache passthrough
// ---------------------------------------------------------------------------
describe("MemoryArbiter — vision embedding cache", () => {
it("returns null on miss and the entry on hit", () => {
const { arbiter } = makeArbiter();
expect(arbiter.getCachedVisionEmbedding("missing")).toBeNull();
const tokens = new Float32Array(8);
arbiter.setCachedVisionEmbedding("h1", { tokens, tokenCount: 2, hiddenSize: 4 });
const hit = arbiter.getCachedVisionEmbedding("h1");
expect(hit?.tokenCount).toBe(2);
expect(hit?.hiddenSize).toBe(4);
expect(hit?.tokens).toBe(tokens);
});
});
describe("VisionEmbeddingCache", () => {
it("evicts LRU when capacity is exceeded", () => {
const cache = new VisionEmbeddingCache({ config: { maxEntries: 2, defaultTtlMs: 60_000 } });
cache.set("a", { tokens: new Float32Array(2), tokenCount: 1, hiddenSize: 2 });
cache.set("b", { tokens: new Float32Array(2), tokenCount: 1, hiddenSize: 2 });
// Touch a so it becomes most recently used.
expect(cache.get("a")).not.toBeNull();
cache.set("c", { tokens: new Float32Array(2), tokenCount: 1, hiddenSize: 2 });
expect(cache.get("b")).toBeNull(); // b was the LRU
expect(cache.get("a")).not.toBeNull();
expect(cache.get("c")).not.toBeNull();
});
it("returns null after TTL expiry and purges via purgeExpired", () => {
let nowMs = 1000;
const cache = new VisionEmbeddingCache({
config: { maxEntries: 8, defaultTtlMs: 100 },
now: () => nowMs,
});
cache.set("a", { tokens: new Float32Array(2), tokenCount: 1, hiddenSize: 2 });
nowMs = 200; // not expired yet (1000 + 100 = 1100; 200 < 1100? wait — re-derive)
// Actually expiry uses now+ttl at insertion. Recompute deterministically.
// a expires at 1000 + 100 = 1100. Bump now past that.
nowMs = 1500;
expect(cache.get("a")).toBeNull();
// Re-insert and purge.
cache.set("b", { tokens: new Float32Array(2), tokenCount: 1, hiddenSize: 2 });
nowMs = 1500 + 200;
const removed = cache.purgeExpired();
expect(removed).toBe(1);
expect(cache.size()).toBe(0);
});
it("rejects mismatched token buffer length", () => {
const cache = new VisionEmbeddingCache();
expect(() =>
cache.set("bad", { tokens: new Float32Array(3), tokenCount: 2, hiddenSize: 2 }),
).toThrow(/does not match/);
});
});
// ---------------------------------------------------------------------------
// Pressure source contracts
// ---------------------------------------------------------------------------
describe("nodeOsPressureSource", () => {
it("emits nominal when free memory is comfortably above the low-water line", () => {
const events: MemoryPressureEvent[] = [];
const source = nodeOsPressureSource(
{ intervalMs: 1_000, lowWaterFraction: 0.15, criticalWaterFraction: 0.05 },
{ osMemory: () => ({ freeBytes: 8 * 1024 ** 3, totalBytes: 16 * 1024 ** 3 }) },
);
const unsub = source.subscribe((e) => events.push(e));
source.start();
expect(events.at(-1)?.level).toBe("nominal");
unsub();
source.stop();
});
it("transitions to low and critical as free memory drops", () => {
let freeBytes = 8 * 1024 ** 3;
const source = nodeOsPressureSource(
{ intervalMs: 1_000, lowWaterFraction: 0.2, criticalWaterFraction: 0.05 },
{ osMemory: () => ({ freeBytes, totalBytes: 16 * 1024 ** 3 }) },
);
expect(source.current().level).toBe("nominal");
freeBytes = 2 * 1024 ** 3; // 12.5% free → below 20% low-water
expect(source.current().level).toBe("low");
freeBytes = 0.5 * 1024 ** 3; // ~3% → below critical
expect(source.current().level).toBe("critical");
});
it("rejects criticalWaterFraction >= lowWaterFraction at construction", () => {
expect(() =>
nodeOsPressureSource({ lowWaterFraction: 0.1, criticalWaterFraction: 0.2 }),
).toThrow(/criticalWaterFraction/);
});
});
describe("capacitorPressureSource", () => {
it("dispatches the level the native bridge sends", () => {
const source = capacitorPressureSource();
const events: MemoryPressureEvent[] = [];
source.subscribe((e) => events.push(e));
source.start();
source.dispatch("low", 256);
source.dispatch("critical", 64);
const levels = events.map((e) => e.level);
expect(levels).toContain("low");
expect(levels).toContain("critical");
});
});
describe("compositePressureSource", () => {
it("reports the worst level across underlying sources", () => {
const a = capacitorPressureSource();
const b = capacitorPressureSource();
const composite = compositePressureSource([a, b]);
composite.start();
const events: MemoryPressureEvent[] = [];
composite.subscribe((e) => events.push(e));
a.dispatch("low");
b.dispatch("critical");
expect(events.at(-1)?.level).toBe("critical");
// Recovery from critical only when *both* report nominal.
b.dispatch("nominal");
expect(events.at(-1)?.level).toBe("low");
a.dispatch("nominal");
expect(events.at(-1)?.level).toBe("nominal");
});
});
// ---------------------------------------------------------------------------
// Concurrency edge cases (self-criticism scenarios)
// ---------------------------------------------------------------------------
describe("MemoryArbiter — concurrency edge cases", () => {
it("does NOT serialize across different capabilities", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
text.runDelayMs.value = 30;
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
const start = Date.now();
// Fire a slow text request, then a vision request. Different
// capabilities have separate queues — they should overlap.
const [t, v] = await Promise.all([
arbiter.requestText<{ x: string }, string>({
modelKey: "tier-2b",
payload: { x: "slow" },
}),
arbiter.requestVisionDescribe<{ x: string }, string>({
modelKey: "gemma-vl-4b",
payload: { x: "fast" },
}),
]);
const elapsed = Date.now() - start;
expect(t).toBe("tier-2b:slow");
expect(v).toBe("gemma-vl-4b:fast");
// If queues were shared, total would be ~30ms (text) + ~0ms (vision) =
// 30ms. With separate queues we expect at most max(loadtimes)+max(runtimes),
// still bounded — assert specifically that vision did not block on text.
expect(elapsed).toBeLessThan(200);
});
it("survives a pressure event arriving during a load", async () => {
const { arbiter, bridge } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
vision.loadDelayMs.value = 40;
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
// Start a slow vision load.
const visionAcquire = arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
// While it's mid-flight, dispatch low pressure. The arbiter should
// process the event without crashing the load.
bridge.dispatch("low");
await new Promise<void>((r) => setTimeout(r, 5));
const handle = await visionAcquire;
expect(handle.backend.id).toBe("gemma-vl-4b");
await handle.release();
});
it("redundant release() is idempotent and does not double-decrement", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
arbiter.registerCapability(text.registration);
const h = await arbiter.acquire<FakeBackend>("text", "tier-2b");
await h.release();
await h.release(); // no-op
// Re-acquire should not need to reload — the role is still warm.
const h2 = await arbiter.acquire<FakeBackend>("text", "tier-2b");
expect(text.loaded).toEqual(["tier-2b"]);
await h2.release();
});
it("retain() throws after release()", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
arbiter.registerCapability(text.registration);
const h = await arbiter.acquire<FakeBackend>("text", "tier-2b");
await h.release();
expect(() => h.retain()).toThrow(/cannot retain/);
});
});
// ---------------------------------------------------------------------------
// Shutdown
// ---------------------------------------------------------------------------
describe("MemoryArbiter — shutdown", () => {
it("unloads every resident handle and rejects new acquires", async () => {
const { arbiter } = makeArbiter();
const text = makeFakeCapability();
const vision = makeFakeCapability({
capability: "vision-describe",
residentRole: "vision",
});
arbiter.registerCapability(text.registration);
arbiter.registerCapability(vision.registration);
const t = await arbiter.acquire<FakeBackend>("text", "tier-2b");
const v = await arbiter.acquire<FakeBackend>("vision-describe", "gemma-vl-4b");
await t.release();
await v.release();
await arbiter.shutdown();
expect(text.unloaded).toContain("tier-2b");
expect(vision.unloaded).toContain("gemma-vl-4b");
await expect(arbiter.acquire("text", "tier-2b")).rejects.toThrow(/shutting down/);
});
});