elizaos--eliza
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
218 行
6.8 KiB
TypeScript
218 行
6.8 KiB
TypeScript
/**
|
|
* Tests for the IDENTIFY_SPEAKER action (issue #8234, shape #2).
|
|
*
|
|
* The action selects the most-recent unidentified speaker profile and
|
|
* drives the merge engine via `VOICE_TURN_OBSERVED`. Here the merge-engine
|
|
* round-trip is simulated by a fake runtime whose `emitEvent` mints an
|
|
* entity id and invokes the real `handleVoiceEntityBound` consumer — so the
|
|
* test exercises the full producer → bind path without loading lifeops.
|
|
*/
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import type { IAgentRuntime, Memory } from "@elizaos/core";
|
|
import { EventType } from "@elizaos/core";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
extractSpeakerName,
|
|
identifySpeakerAction,
|
|
} from "../src/actions/identify-speaker";
|
|
import {
|
|
handleVoiceEntityBound,
|
|
setVoiceEntityBindingStore,
|
|
} from "../src/runtime/voice-entity-binding";
|
|
import { VoiceProfileStore } from "../src/services/voice/profile-store";
|
|
import { WESPEAKER_RESNET34_LM_INT8_MODEL_ID } from "../src/services/voice/speaker/encoder";
|
|
|
|
const MODEL = WESPEAKER_RESNET34_LM_INT8_MODEL_ID;
|
|
|
|
let tmpRoot: string;
|
|
let store: VoiceProfileStore;
|
|
|
|
function unit(values: number[]): Float32Array {
|
|
let sumSq = 0;
|
|
for (const v of values) sumSq += v * v;
|
|
const inv = sumSq > 0 ? 1 / Math.sqrt(sumSq) : 1;
|
|
return new Float32Array(values.map((v) => v * inv));
|
|
}
|
|
|
|
function makeMessage(text: string): Memory {
|
|
return {
|
|
id: "11111111-1111-1111-1111-111111111111",
|
|
entityId: "22222222-2222-2222-2222-222222222222",
|
|
roomId: "33333333-3333-3333-3333-333333333333",
|
|
content: { text },
|
|
} as unknown as Memory;
|
|
}
|
|
|
|
/**
|
|
* Fake runtime whose `emitEvent` plays the merge-engine consumer: on a
|
|
* VOICE_TURN_OBSERVED it mints an entity id and runs the real round-trip
|
|
* binding handler.
|
|
*/
|
|
function makeRuntime(entityId: string): {
|
|
runtime: IAgentRuntime;
|
|
emitEvent: ReturnType<typeof vi.fn>;
|
|
} {
|
|
const emitEvent = vi.fn(
|
|
async (type: string, payload: { imprintClusterId: string; text: string }) => {
|
|
if (type === EventType.VOICE_TURN_OBSERVED) {
|
|
await handleVoiceEntityBound({
|
|
runtime: {} as IAgentRuntime,
|
|
imprintClusterId: payload.imprintClusterId,
|
|
entityId,
|
|
displayName: payload.text.replace(/^This is\s+/, "").replace(/\.$/, ""),
|
|
});
|
|
}
|
|
},
|
|
);
|
|
return { runtime: { emitEvent } as unknown as IAgentRuntime, emitEvent };
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
tmpRoot = mkdtempSync(path.join(tmpdir(), "identify-speaker-"));
|
|
store = new VoiceProfileStore({ rootDir: tmpRoot });
|
|
await store.init();
|
|
setVoiceEntityBindingStore(store);
|
|
});
|
|
|
|
afterEach(() => {
|
|
setVoiceEntityBindingStore(null);
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("extractSpeakerName", () => {
|
|
it.each([
|
|
["that was Jill", "Jill"],
|
|
["this is my friend Sam", "Sam"],
|
|
["call her Alex", "Alex"],
|
|
["his name is Bob Smith", "Bob Smith"],
|
|
["the speaker was Dana", "Dana"],
|
|
])("extracts %s → %s", (input, expected) => {
|
|
expect(extractSpeakerName(input)).toBe(expected);
|
|
});
|
|
|
|
it.each(["hello there", "what time is it", ""])(
|
|
"returns null for non-claim %s",
|
|
(input) => {
|
|
expect(extractSpeakerName(input)).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("identifySpeakerAction", () => {
|
|
it("binds the most-recent unidentified speaker to the named entity", async () => {
|
|
// An already-identified profile must be skipped.
|
|
await store.createProfile({
|
|
centroid: unit([1, 0, 0, 0]),
|
|
embeddingModel: MODEL,
|
|
imprintClusterId: "cluster_known",
|
|
entityId: "ent_known",
|
|
confidence: 0.9,
|
|
durationMs: 1500,
|
|
});
|
|
const unknown = await store.createProfile({
|
|
centroid: unit([0, 1, 0, 0]),
|
|
embeddingModel: MODEL,
|
|
imprintClusterId: "cluster_unknown",
|
|
confidence: 0.5,
|
|
durationMs: 1500,
|
|
});
|
|
|
|
const { runtime, emitEvent } = makeRuntime("ent_jill");
|
|
const result = await identifySpeakerAction.handler!(
|
|
runtime,
|
|
makeMessage("that was Jill"),
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
|
|
expect(emitEvent).toHaveBeenCalledTimes(1);
|
|
const [eventType, payload] = emitEvent.mock.calls[0] as [
|
|
string,
|
|
{ imprintClusterId: string; text: string; matchedEntityId: string | null },
|
|
];
|
|
expect(eventType).toBe(EventType.VOICE_TURN_OBSERVED);
|
|
expect(payload.imprintClusterId).toBe("cluster_unknown");
|
|
expect(payload.text).toBe("This is Jill.");
|
|
expect(payload.matchedEntityId).toBeNull();
|
|
|
|
expect(result).toMatchObject({ success: true });
|
|
expect((result as { data?: Record<string, unknown> }).data).toMatchObject({
|
|
profileId: unknown.profileId,
|
|
entityId: "ent_jill",
|
|
name: "Jill",
|
|
});
|
|
// The profile is now bound.
|
|
expect((await store.get(unknown.profileId))?.entityId).toBe("ent_jill");
|
|
});
|
|
|
|
it("fails cleanly when no name can be resolved", async () => {
|
|
const { runtime, emitEvent } = makeRuntime("ent_x");
|
|
const result = await identifySpeakerAction.handler!(
|
|
runtime,
|
|
makeMessage("who was that"),
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(result).toMatchObject({ success: false });
|
|
expect(emitEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails cleanly when there is no unidentified recent voice", async () => {
|
|
await store.createProfile({
|
|
centroid: unit([1, 0, 0, 0]),
|
|
embeddingModel: MODEL,
|
|
imprintClusterId: "cluster_known",
|
|
entityId: "ent_known",
|
|
confidence: 0.9,
|
|
durationMs: 1500,
|
|
});
|
|
const { runtime, emitEvent } = makeRuntime("ent_x");
|
|
const result = await identifySpeakerAction.handler!(
|
|
runtime,
|
|
makeMessage("that was Jill"),
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(result).toMatchObject({ success: false });
|
|
expect(emitEvent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("honors an explicit profileId option", async () => {
|
|
const a = await store.createProfile({
|
|
centroid: unit([0, 1, 0, 0]),
|
|
embeddingModel: MODEL,
|
|
imprintClusterId: "cluster_a",
|
|
confidence: 0.5,
|
|
durationMs: 1500,
|
|
});
|
|
await store.createProfile({
|
|
centroid: unit([0, 0, 1, 0]),
|
|
embeddingModel: MODEL,
|
|
imprintClusterId: "cluster_b",
|
|
confidence: 0.5,
|
|
durationMs: 1500,
|
|
});
|
|
|
|
const { runtime, emitEvent } = makeRuntime("ent_target");
|
|
await identifySpeakerAction.handler!(
|
|
runtime,
|
|
makeMessage("name this voice"),
|
|
undefined,
|
|
{ name: "Dana", profileId: a.profileId },
|
|
undefined,
|
|
);
|
|
const [, payload] = emitEvent.mock.calls[0] as [
|
|
string,
|
|
{ imprintClusterId: string },
|
|
];
|
|
expect(payload.imprintClusterId).toBe("cluster_a");
|
|
expect((await store.get(a.profileId))?.entityId).toBe("ent_target");
|
|
});
|
|
});
|