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
249 lines
8.1 KiB
TypeScript
249 lines
8.1 KiB
TypeScript
/**
|
|
* LifeOps relationships integration tests against a real PGLite runtime.
|
|
*
|
|
* Exercises the LifeOpsService relationship surface and the canonical
|
|
* ENTITY action handler end-to-end. No SQL mocks, no LLM — the action
|
|
* handler is invoked with an explicit `subaction` so the planner LLM path
|
|
* is skipped and only the deterministic branches run. Follow-up cadence
|
|
* lives on `SCHEDULED_TASK` and is covered by `scheduled-task-action.test.ts`.
|
|
*/
|
|
|
|
import { KnowledgeGraphService, knowledgeGraphSchema } from "@elizaos/agent";
|
|
import type { AgentRuntime, IAgentRuntime, Plugin } from "@elizaos/core";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
createRealTestRuntime,
|
|
type RealTestRuntimeResult,
|
|
} from "../../../packages/test/helpers/real-runtime.ts";
|
|
import { entityAction } from "../src/actions/entity.ts";
|
|
import { LifeOpsRepository } from "../src/lifeops/repository.ts";
|
|
import { LifeOpsService } from "../src/lifeops/service.ts";
|
|
import {
|
|
acceptCanonicalIdentityMerge,
|
|
assertCanonicalIdentityMerged,
|
|
CANONICAL_IDENTITY_PLATFORMS,
|
|
getCanonicalIdentityGraph,
|
|
getCanonicalPersonDetail,
|
|
seedCanonicalIdentityFixture,
|
|
} from "./helpers/lifeops-identity-merge-fixtures.ts";
|
|
|
|
const AGENT_ID = "lifeops-relationships-agent";
|
|
|
|
/**
|
|
* Minimal stand-in for the production "eliza" plugin: registers the
|
|
* knowledge-graph schema + KnowledgeGraphService so contacts (which live in
|
|
* the runtime graph) resolve through `resolveKnowledgeGraphService`.
|
|
*/
|
|
const knowledgeGraphPlugin: Plugin = {
|
|
name: "eliza",
|
|
description: "Test-only knowledge-graph schema + service registration.",
|
|
schema: knowledgeGraphSchema,
|
|
services: [KnowledgeGraphService],
|
|
};
|
|
|
|
function makeMessage(runtime: IAgentRuntime, text: string) {
|
|
return {
|
|
id: `msg-${Math.random()}` as string,
|
|
entityId: runtime.agentId,
|
|
roomId: runtime.agentId,
|
|
content: { text },
|
|
};
|
|
}
|
|
|
|
function getEntityActionHandler() {
|
|
const { handler } = entityAction;
|
|
if (!handler) {
|
|
throw new Error("entityAction handler is required for relationships tests");
|
|
}
|
|
return handler;
|
|
}
|
|
|
|
describe("relationships handler — real PGLite", () => {
|
|
let runtime: AgentRuntime;
|
|
let service: LifeOpsService;
|
|
let testResult: RealTestRuntimeResult;
|
|
|
|
beforeAll(async () => {
|
|
testResult = await createRealTestRuntime({
|
|
characterName: AGENT_ID,
|
|
plugins: [knowledgeGraphPlugin],
|
|
});
|
|
runtime = testResult.runtime;
|
|
await LifeOpsRepository.bootstrapSchema(runtime);
|
|
service = new LifeOpsService(runtime);
|
|
}, 180_000);
|
|
|
|
afterAll(async () => {
|
|
await testResult?.cleanup();
|
|
});
|
|
|
|
it("upsertRelationship persists and listRelationships returns it", async () => {
|
|
const rel = await service.upsertRelationship({
|
|
name: "Alice",
|
|
primaryChannel: "email",
|
|
primaryHandle: "alice@example.com",
|
|
email: "alice@example.com",
|
|
phone: null,
|
|
notes: "test",
|
|
tags: ["friend"],
|
|
relationshipType: "friend",
|
|
lastContactedAt: null,
|
|
metadata: {},
|
|
});
|
|
expect(rel.id).toBeTruthy();
|
|
const list = await service.listRelationships({});
|
|
expect(list.find((r) => r.id === rel.id)).toBeTruthy();
|
|
});
|
|
|
|
it("logInteraction updates lastContactedAt and getDaysSinceContact returns 0", async () => {
|
|
const rel = await service.upsertRelationship({
|
|
name: "Bob",
|
|
primaryChannel: "email",
|
|
primaryHandle: "bob@example.com",
|
|
email: "bob@example.com",
|
|
phone: null,
|
|
notes: "",
|
|
tags: [],
|
|
relationshipType: "contact",
|
|
lastContactedAt: null,
|
|
metadata: {},
|
|
});
|
|
await service.logInteraction({
|
|
relationshipId: rel.id,
|
|
channel: "email",
|
|
direction: "outbound",
|
|
summary: "checked in",
|
|
occurredAt: new Date().toISOString(),
|
|
metadata: {},
|
|
});
|
|
const days = await service.getDaysSinceContact(rel.id);
|
|
expect(days).toBe(0);
|
|
});
|
|
|
|
it("entityAction list handler returns ActionResult", async () => {
|
|
const result = await getEntityActionHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "show me my contacts") as never,
|
|
undefined,
|
|
{ parameters: { subaction: "list" } } as never,
|
|
async () => {},
|
|
);
|
|
expect(result?.success).toBe(true);
|
|
const data = (result as { data?: { contacts?: unknown[] } }).data;
|
|
expect(Array.isArray(data?.contacts)).toBe(true);
|
|
});
|
|
|
|
it("entityAction add handler persists a new contact", async () => {
|
|
const result = await getEntityActionHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "add Eve to rolodex") as never,
|
|
undefined,
|
|
{
|
|
parameters: {
|
|
subaction: "add",
|
|
name: "Eve",
|
|
channel: "telegram",
|
|
handle: "@eve",
|
|
},
|
|
} as never,
|
|
async () => {},
|
|
);
|
|
expect(result?.success).toBe(true);
|
|
const data = (
|
|
result as {
|
|
data?: { relationship?: { id: string; name: string } };
|
|
}
|
|
).data;
|
|
expect(data?.relationship?.name).toBe("Eve");
|
|
const list = await service.listRelationships({});
|
|
expect(list.find((r) => r.name === "Eve")).toBeTruthy();
|
|
});
|
|
|
|
it("entityAction add rejects missing fields", async () => {
|
|
const result = await getEntityActionHandler()(
|
|
runtime,
|
|
makeMessage(runtime, "add contact") as never,
|
|
undefined,
|
|
{ parameters: { subaction: "add", name: "OnlyName" } } as never,
|
|
async () => {},
|
|
);
|
|
expect(result?.success).toBe(false);
|
|
expect((result as { data?: { error?: string } }).data?.error).toBe(
|
|
"MISSING_FIELDS",
|
|
);
|
|
});
|
|
|
|
// Follow-up cadence (`days_since`, `list_overdue_followups`,
|
|
// `set_followup_threshold`) lives on the SCHEDULED_TASK umbrella. Overdue
|
|
// follow-ups are derived from contact cadence by `computeOverdueFollowups`
|
|
// (followup-tracker) over the runtime knowledge graph; there is no separate
|
|
// LifeOps follow-up table. These are exercised by the followup-tracker tests
|
|
// and `scheduled-task-action.test.ts`.
|
|
|
|
it("relationships graph collapses a four-platform person into one canonical node after accepted merges", async () => {
|
|
const fixture = await seedCanonicalIdentityFixture({
|
|
runtime,
|
|
seedKey: "real-graph-merge",
|
|
personName: "Priya Rao Graph Merge",
|
|
});
|
|
|
|
const before = await (
|
|
await getCanonicalIdentityGraph(runtime)
|
|
).getGraphSnapshot({
|
|
search: fixture.personName,
|
|
limit: 10,
|
|
});
|
|
expect(before.people).toHaveLength(CANONICAL_IDENTITY_PLATFORMS.length);
|
|
|
|
await acceptCanonicalIdentityMerge(runtime, fixture);
|
|
|
|
const mergedCheck = await assertCanonicalIdentityMerged({
|
|
runtime,
|
|
personName: fixture.personName,
|
|
});
|
|
expect(mergedCheck).toBeUndefined();
|
|
|
|
const after = await (
|
|
await getCanonicalIdentityGraph(runtime)
|
|
).getGraphSnapshot({
|
|
search: fixture.personName,
|
|
limit: 10,
|
|
});
|
|
expect(after.people).toHaveLength(1);
|
|
expect(after.people[0]?.primaryEntityId).toBe(fixture.primaryEntityId);
|
|
});
|
|
|
|
it("person detail exposes all merged identities and cross-platform conversations", async () => {
|
|
const fixture = await seedCanonicalIdentityFixture({
|
|
runtime,
|
|
seedKey: "real-person-detail",
|
|
personName: "Priya Rao Detail",
|
|
});
|
|
await acceptCanonicalIdentityMerge(runtime, fixture);
|
|
|
|
const detail = await getCanonicalPersonDetail(runtime, fixture.personName);
|
|
expect(detail).toBeTruthy();
|
|
expect(detail?.memberEntityIds).toHaveLength(
|
|
CANONICAL_IDENTITY_PLATFORMS.length,
|
|
);
|
|
expect(detail?.identities).toHaveLength(
|
|
CANONICAL_IDENTITY_PLATFORMS.length,
|
|
);
|
|
expect(detail?.recentConversations).toHaveLength(
|
|
CANONICAL_IDENTITY_PLATFORMS.length,
|
|
);
|
|
expect(detail?.identityEdges).toHaveLength(
|
|
CANONICAL_IDENTITY_PLATFORMS.length - 1,
|
|
);
|
|
const transcript =
|
|
detail?.recentConversations
|
|
.flatMap((entry) => entry.messages.map((message) => message.text))
|
|
.join("\n") ?? "";
|
|
expect(transcript).toContain("Gmail:");
|
|
expect(transcript).toContain("Signal:");
|
|
expect(transcript).toContain("Telegram:");
|
|
expect(transcript).toContain("WhatsApp:");
|
|
expect(transcript).toContain("Discord:");
|
|
});
|
|
});
|