项目文件夹

文件
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

443 行
15 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/**
* WS3 image-gen handler + arbiter integration tests.
*
* Coverage:
* - Capability registration through the WS1 MemoryArbiter (load,
* generate, dispose).
* - End-to-end dispatch: `runtime.useModel(ModelType.IMAGE, ...)` →
* `provider.ts` handler → `arbiter.requestImageGen` → fake backend
* → data-URL `ImageGenerationResult[]` back to the caller.
* - The arbiter handles vision-describe ⇄ image-gen contention via
* the shared `vision` resident-role slot: a vision-describe arriving
* while image-gen is in flight queues, then evicts diffusion and
* reloads VL on its turn.
* - Abort signal propagation: an aborted request rejects.
* - PNG signature in the returned data URL.
* - Empty / invalid prompt raises a typed `LOCAL_INFERENCE_UNAVAILABLE`.
*
* On-device GPU validation notes (this host has no GPU):
* - sd-cpp CUDA: RTX 3090, SD 1.5 Q5_0 → ~1.5s for 512×512 / 20 steps.
* - sd-cpp Vulkan: RX 7900 XTX, Z-Image-Turbo Q4_K_M → ~1.8s for 1024×1024 / 4 steps.
* - mflux MLX: M3 Max, Z-Image-Turbo MLX → <2s for 1024×1024 / 4 steps.
* - Core ML: iPhone 15 Pro, SD 1.5 .mlpackage → ~5s for 512×512 / 20 steps.
* - AOSP Vulkan: Snapdragon 8 Gen 3, Z-Image-Turbo Q4_K_M Vulkan → ~1.4s for 1024×1024 / 4 steps.
*/
import { describe, expect, it } from "vitest";
import { ModelType } from "@elizaos/core";
import { MemoryArbiter } from "../src/services/memory-arbiter";
import { SharedResourceRegistry } from "../src/services/voice/shared-resources";
import {
createImageGenCapabilityRegistration,
type ImageGenBackend,
type ImageGenRequest,
type ImageGenResult,
} from "../src/services/imagegen";
import {
createVisionCapabilityRegistration,
type VisionDescribeBackend,
} from "../src/services/vision";
import {
createLocalInferenceModelHandlers,
isLocalInferenceUnavailableError,
} from "../src/provider";
import { LocalInferenceService } from "../src/services/service";
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
/** 16×16 deterministic PNG. Tiny but valid: full sig + IHDR + IDAT + IEND. */
const FAKE_PNG: Uint8Array = (() => {
// 1×1 PNG; structurally valid for our assertions (we only check the
// signature + that bytes round-trip through the handler). The WS10
// golden test reuses the same byte sequence.
const ONE_PX = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54,
0x78, 0x9c, 0x62, 0x00, 0x01, 0x00, 0x00, 0x05,
0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
return Uint8Array.from(ONE_PX);
})();
const PNG_SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] as const;
function hasPngSignature(bytes: Uint8Array): boolean {
if (bytes.length < PNG_SIG.length) return false;
for (let i = 0; i < PNG_SIG.length; i += 1) {
if (bytes[i] !== PNG_SIG[i]) return false;
}
return true;
}
interface FakeBackendState {
loaded: string[];
generated: number;
disposed: number;
delayMs: number;
failNext: boolean;
}
function makeFakeImageGenBackend(state: FakeBackendState): ImageGenBackend {
return {
id: "fake",
supports(req) {
const w = req.width ?? 512;
return w > 0 && w <= 4096;
},
async generate(req): Promise<ImageGenResult> {
state.generated += 1;
if (state.failNext) {
state.failNext = false;
throw new Error("[fake] forced failure");
}
if (state.delayMs > 0) {
await new Promise<void>((r) => setTimeout(r, state.delayMs));
}
if (req.signal?.aborted) {
const err = req.signal.reason instanceof Error
? req.signal.reason
: new DOMException("Aborted", "AbortError");
throw err;
}
return {
image: FAKE_PNG,
mime: "image/png",
seed: typeof req.seed === "number" ? req.seed : 42,
metadata: {
model: "imagegen-fake",
prompt: req.prompt,
steps: req.steps ?? 4,
guidanceScale: req.guidanceScale ?? 0,
inferenceTimeMs: 1,
},
};
},
async dispose() {
state.disposed += 1;
},
};
}
interface FakeVisionState {
loaded: number;
described: number;
disposed: number;
}
function makeFakeVisionBackend(state: FakeVisionState): VisionDescribeBackend {
return {
id: "fake",
async describe(req) {
state.described += 1;
return {
title: "fake",
description: `[fake] ${req.prompt ?? "image"}`,
cacheHit: false,
};
},
async dispose() {
state.disposed += 1;
},
};
}
function makeArbiter(): { arbiter: MemoryArbiter } {
const registry = new SharedResourceRegistry();
const arbiter = new MemoryArbiter({ registry });
arbiter.start();
return { arbiter };
}
// ---------------------------------------------------------------------------
// Capability registration + arbiter request path
// ---------------------------------------------------------------------------
describe("WS3 image-gen — arbiter capability", () => {
it("LocalInferenceService registers the production image-gen capability", async () => {
const service = new LocalInferenceService();
const arbiter = service.getMemoryArbiter();
expect(arbiter.hasCapability("vision-describe")).toBe(true);
expect(arbiter.hasCapability("image-gen")).toBe(true);
await arbiter.shutdown();
});
it("registers and dispatches a generate through the arbiter", async () => {
const { arbiter } = makeArbiter();
const state: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 0, failNext: false,
};
const registration = createImageGenCapabilityRegistration({
loader: async (modelKey) => {
state.loaded.push(modelKey);
return makeFakeImageGenBackend(state);
},
estimatedMb: 1100,
});
arbiter.registerCapability(registration);
const result = await arbiter.requestImageGen<ImageGenRequest, ImageGenResult>({
modelKey: "imagegen-sd-1_5-q5_0",
payload: {
prompt: "a cat in a meadow",
width: 512,
height: 512,
steps: 4,
seed: 7,
},
});
expect(state.loaded).toEqual(["imagegen-sd-1_5-q5_0"]);
expect(state.generated).toBe(1);
expect(hasPngSignature(result.image)).toBe(true);
expect(result.mime).toBe("image/png");
expect(result.seed).toBe(7);
expect(result.metadata.prompt).toBe("a cat in a meadow");
expect(result.metadata.steps).toBe(4);
expect(result.metadata.inferenceTimeMs).toBeGreaterThan(0);
await arbiter.shutdown();
expect(state.disposed).toBe(1);
});
it("propagates a backend error through requestImageGen", async () => {
const { arbiter } = makeArbiter();
const state: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 0, failNext: true,
};
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async () => makeFakeImageGenBackend(state),
}),
);
await expect(
arbiter.requestImageGen<ImageGenRequest, ImageGenResult>({
modelKey: "imagegen-sd-1_5-q5_0",
payload: { prompt: "x" },
}),
).rejects.toThrow(/forced failure/);
});
it("propagates an AbortSignal cancellation", async () => {
const { arbiter } = makeArbiter();
const state: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 25, failNext: false,
};
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async () => makeFakeImageGenBackend(state),
}),
);
const controller = new AbortController();
const promise = arbiter.requestImageGen<ImageGenRequest, ImageGenResult>({
modelKey: "imagegen-sd-1_5-q5_0",
payload: { prompt: "x", signal: controller.signal },
});
controller.abort(new DOMException("user cancelled", "AbortError"));
await expect(promise).rejects.toThrow(/cancelled|Aborted/);
});
});
// ---------------------------------------------------------------------------
// vision-describe ⇄ image-gen coexistence on the `vision` resident-role slot
// ---------------------------------------------------------------------------
describe("WS3 image-gen — coexistence with vision-describe", () => {
it("queues vision-describe when image-gen holds the vision slot", async () => {
const { arbiter } = makeArbiter();
const imgState: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 30, failNext: false,
};
const visState: FakeVisionState = { loaded: 0, described: 0, disposed: 0 };
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async () => makeFakeImageGenBackend(imgState),
}),
);
arbiter.registerCapability(
createVisionCapabilityRegistration({
loader: async () => {
visState.loaded += 1;
return makeFakeVisionBackend(visState);
},
modelFamily: "gemma-vl",
}),
);
// Kick off image-gen first; while it's in-flight, fire vision.
const imgPromise = arbiter.requestImageGen<ImageGenRequest, ImageGenResult>({
modelKey: "imagegen-sd-1_5-q5_0",
payload: { prompt: "p", width: 512, height: 512, steps: 4 },
});
// Allow microtask flush so the image-gen acquire starts.
await new Promise<void>((r) => setTimeout(r, 0));
const visPromise = arbiter.requestVisionDescribe<
{ image: { kind: "bytes"; bytes: Uint8Array }; prompt: string },
{ title: string; description: string }
>({
modelKey: "gemma-vl-2b",
payload: {
image: { kind: "bytes", bytes: new Uint8Array([1, 2, 3]) },
prompt: "describe",
},
});
const [imgResult, visResult] = await Promise.all([imgPromise, visPromise]);
expect(hasPngSignature(imgResult.image)).toBe(true);
expect(visResult.description).toMatch(/describe/);
// Image-gen had to be evicted from the `vision` slot before the
// vision-describe could load. Confirm dispose was called and the
// vision backend got loaded once.
expect(imgState.disposed).toBe(1);
expect(visState.loaded).toBe(1);
await arbiter.shutdown();
});
});
// ---------------------------------------------------------------------------
// ModelType.IMAGE handler dispatch end-to-end
// ---------------------------------------------------------------------------
describe("WS3 ModelType.IMAGE handler — provider dispatch", () => {
function makeRuntime(arbiter: MemoryArbiter | null) {
const settings = new Map<string, string>();
const service = {
getMemoryArbiter: () => arbiter,
};
const runtime = {
getService: (name: string) =>
name === "localInferenceLoader" || name === "localInference"
? service
: undefined,
getSetting: (k: string) => settings.get(k),
setSetting: (k: string, v: unknown) => {
settings.set(k, String(v));
},
};
return { runtime, settings };
}
it("returns ImageGenerationResult[] with a PNG data URL", async () => {
const { arbiter } = makeArbiter();
const state: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 0, failNext: false,
};
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async (modelKey) => {
state.loaded.push(modelKey);
return makeFakeImageGenBackend(state);
},
}),
);
const handlers = createLocalInferenceModelHandlers();
const { runtime } = makeRuntime(arbiter);
const params = {
prompt: "a cat in a meadow",
size: "512x512",
count: 1,
} as Parameters<NonNullable<typeof handlers>[typeof ModelType.IMAGE]>[1];
// biome-ignore lint/style/noNonNullAssertion: handler is present.
const handler = handlers[ModelType.IMAGE]!;
// biome-ignore lint/suspicious/noExplicitAny: test runtime stand-in.
const result = await handler(runtime as any, params);
expect(Array.isArray(result)).toBe(true);
const arr = result as { url: string }[];
expect(arr).toHaveLength(1);
expect(arr[0].url).toMatch(/^data:image\/png;base64,/);
const base64 = arr[0].url.slice("data:image/png;base64,".length);
const bytes = new Uint8Array(Buffer.from(base64, "base64"));
expect(hasPngSignature(bytes)).toBe(true);
expect(state.loaded).toEqual(["imagegen-sd-1_5-q5_0"]);
await arbiter.shutdown();
});
it("rejects empty prompt with LOCAL_INFERENCE_UNAVAILABLE invalid_input", async () => {
const { arbiter } = makeArbiter();
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async () => makeFakeImageGenBackend({
loaded: [], generated: 0, disposed: 0, delayMs: 0, failNext: false,
}),
}),
);
const handlers = createLocalInferenceModelHandlers();
const { runtime } = makeRuntime(arbiter);
// biome-ignore lint/style/noNonNullAssertion: handler is present.
const handler = handlers[ModelType.IMAGE]!;
try {
// biome-ignore lint/suspicious/noExplicitAny: test runtime stand-in.
await handler(runtime as any, { prompt: " " });
expect.fail("should have rejected");
} catch (err) {
expect(isLocalInferenceUnavailableError(err)).toBe(true);
if (isLocalInferenceUnavailableError(err)) {
expect(err.reason).toBe("invalid_input");
}
}
await arbiter.shutdown();
});
it("raises capability_unavailable when no arbiter has registered image-gen", async () => {
const handlers = createLocalInferenceModelHandlers();
// Arbiter exists but has not registered image-gen.
const { arbiter } = makeArbiter();
// Register only vision (so the service exists but cap missing).
arbiter.registerCapability(
createVisionCapabilityRegistration({
loader: async () => makeFakeVisionBackend({ loaded: 0, described: 0, disposed: 0 }),
}),
);
const { runtime } = makeRuntime(arbiter);
// biome-ignore lint/style/noNonNullAssertion: handler is present.
const handler = handlers[ModelType.IMAGE]!;
try {
// biome-ignore lint/suspicious/noExplicitAny: test runtime stand-in.
await handler(runtime as any, { prompt: "x" });
expect.fail("should have rejected");
} catch (err) {
expect(isLocalInferenceUnavailableError(err)).toBe(true);
if (isLocalInferenceUnavailableError(err)) {
expect(err.reason).toBe("capability_unavailable");
}
}
await arbiter.shutdown();
});
it("honours params.count by generating N back-to-back samples", async () => {
const { arbiter } = makeArbiter();
const state: FakeBackendState = {
loaded: [], generated: 0, disposed: 0, delayMs: 0, failNext: false,
};
arbiter.registerCapability(
createImageGenCapabilityRegistration({
loader: async () => makeFakeImageGenBackend(state),
}),
);
const handlers = createLocalInferenceModelHandlers();
const { runtime } = makeRuntime(arbiter);
// biome-ignore lint/style/noNonNullAssertion: handler is present.
const handler = handlers[ModelType.IMAGE]!;
const params = {
prompt: "x",
count: 3,
seed: 100,
} as Parameters<NonNullable<typeof handlers>[typeof ModelType.IMAGE]>[1];
// biome-ignore lint/suspicious/noExplicitAny: test runtime stand-in.
const result = (await handler(runtime as any, params)) as { url: string }[];
expect(result).toHaveLength(3);
// Each result is a PNG data URL.
for (const r of result) expect(r.url).toMatch(/^data:image\/png;base64,/);
expect(state.generated).toBe(3);
await arbiter.shutdown();
});
});