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

265 lines
8.3 KiB
TypeScript

/**
* Tests for the RelationshipStore + extraction helpers.
*
* Covers:
* - multi-type edges between same pair (Pat is colleague_of AND knows
* with separate cadences)
* - RelationshipStore.observe strengthening (adds evidence, bumps
* interactionCount, updates lastInteractionAt)
* - retire-with-audit (soft-delete; new evidence does NOT revive)
* - extraction.ts: "Pat is my manager at Acme" → 2 entities + 3 edges
* - cadenceOverdueAsOf filter
*/
import type { AgentRuntime } from "@elizaos/core";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
createRealTestRuntime,
type RealTestRuntimeResult,
} from "../../../packages/test/helpers/real-runtime.ts";
import { EntityStore } from "../src/lifeops/entities/store";
import { SELF_ENTITY_ID } from "../src/lifeops/entities/types";
import {
applyExtractedEdges,
managerOfAtCompany,
} from "../src/lifeops/relationships/extraction";
import { RelationshipStore } from "../src/lifeops/relationships/store";
import { LifeOpsRepository } from "../src/lifeops/repository";
describe("RelationshipStore — real PGLite", () => {
let runtime: AgentRuntime;
let testResult: RealTestRuntimeResult;
let entityStore: EntityStore;
let store: RelationshipStore;
const agentId = "relationship-graph-tests";
beforeAll(async () => {
testResult = await createRealTestRuntime({ characterName: agentId });
runtime = testResult.runtime;
await LifeOpsRepository.bootstrapSchema(runtime);
entityStore = new EntityStore(runtime, agentId);
store = new RelationshipStore(runtime, agentId);
await entityStore.ensureSelf();
}, 180_000);
afterAll(async () => {
await testResult?.cleanup();
});
it("upsert creates an edge and list returns it", async () => {
const pat = await entityStore.upsert({
type: "person",
preferredName: "Pat",
identities: [],
tags: [],
visibility: "owner_agent_admin",
state: {},
});
const rel = await store.upsert({
fromEntityId: SELF_ENTITY_ID,
toEntityId: pat.entityId,
type: "colleague_of",
metadata: { cadenceDays: 14 },
state: {},
evidence: ["seed"],
confidence: 0.9,
source: "user_chat",
});
const list = await store.list({ fromEntityId: SELF_ENTITY_ID });
expect(
list.find((r) => r.relationshipId === rel.relationshipId),
).toBeTruthy();
});
it("supports multiple typed edges between the same pair (colleague_of + knows)", async () => {
const taylor = await entityStore.upsert({
type: "person",
preferredName: "Taylor",
identities: [],
tags: [],
visibility: "owner_agent_admin",
state: {},
});
const colleague = await store.upsert({
fromEntityId: SELF_ENTITY_ID,
toEntityId: taylor.entityId,
type: "colleague_of",
metadata: { cadenceDays: 14 },
state: {},
evidence: ["c"],
confidence: 0.9,
source: "user_chat",
});
const friend = await store.upsert({
fromEntityId: SELF_ENTITY_ID,
toEntityId: taylor.entityId,
type: "knows",
metadata: { cadenceDays: 30 },
state: {},
evidence: ["k"],
confidence: 0.85,
source: "user_chat",
});
const edges = await store.list({
fromEntityId: SELF_ENTITY_ID,
toEntityId: taylor.entityId,
});
expect(edges.map((e) => e.type).sort()).toEqual(["colleague_of", "knows"]);
expect(colleague.relationshipId).not.toBe(friend.relationshipId);
});
it("observe strengthens an existing matching edge", async () => {
const sam = await entityStore.upsert({
type: "person",
preferredName: "Sam",
identities: [],
tags: [],
visibility: "owner_agent_admin",
state: {},
});
const a = await store.observe({
fromEntityId: SELF_ENTITY_ID,
toEntityId: sam.entityId,
type: "knows",
evidence: ["sam-1"],
confidence: 0.6,
});
const b = await store.observe({
fromEntityId: SELF_ENTITY_ID,
toEntityId: sam.entityId,
type: "knows",
evidence: ["sam-2"],
confidence: 0.7,
});
expect(a.relationshipId).toBe(b.relationshipId);
expect(b.evidence).toContain("sam-1");
expect(b.evidence).toContain("sam-2");
expect(b.confidence).toBeCloseTo(0.7);
expect(b.state.interactionCount).toBe(2);
});
it("retire soft-deletes; observe-on-retired logs evidence without reviving", async () => {
const ex = await entityStore.upsert({
type: "person",
preferredName: "ExColleague",
identities: [],
tags: [],
visibility: "owner_agent_admin",
state: {},
});
const rel = await store.upsert({
fromEntityId: SELF_ENTITY_ID,
toEntityId: ex.entityId,
type: "colleague_of",
metadata: {},
state: {},
evidence: [],
confidence: 0.9,
source: "user_chat",
});
await store.retire(rel.relationshipId, "left_company");
// Default list excludes retired.
const active = await store.list({
fromEntityId: SELF_ENTITY_ID,
toEntityId: ex.entityId,
});
expect(
active.find((r) => r.relationshipId === rel.relationshipId),
).toBeFalsy();
// observe-on-retired logs but does not revive.
const after = await store.observe({
fromEntityId: SELF_ENTITY_ID,
toEntityId: ex.entityId,
type: "colleague_of",
evidence: ["resurrection-attempt"],
confidence: 0.95,
});
expect(after.status).toBe("retired");
const all = await store.list({
fromEntityId: SELF_ENTITY_ID,
toEntityId: ex.entityId,
includeRetired: true,
});
const retired = all.find((r) => r.relationshipId === rel.relationshipId);
expect(retired?.status).toBe("retired");
const audit = await store.listAuditEvents(rel.relationshipId);
expect(audit.find((event) => event.kind === "retire")).toBeTruthy();
expect(
audit.find((event) => event.kind === "observe_on_retired"),
).toBeTruthy();
});
it("cadenceOverdueAsOf returns edges past their cadenceDays threshold", async () => {
const friend = await entityStore.upsert({
type: "person",
preferredName: "OverdueFriend",
identities: [],
tags: [],
visibility: "owner_agent_admin",
state: {},
});
const sevenDaysAgo = new Date(
Date.now() - 7 * 24 * 60 * 60 * 1000,
).toISOString();
await store.upsert({
fromEntityId: SELF_ENTITY_ID,
toEntityId: friend.entityId,
type: "follows",
metadata: { cadenceDays: 3 },
state: { lastInteractionAt: sevenDaysAgo },
evidence: [],
confidence: 0.9,
source: "user_chat",
});
const overdue = await store.list({
cadenceOverdueAsOf: new Date().toISOString(),
});
expect(
overdue.find((rel) => rel.toEntityId === friend.entityId),
).toBeTruthy();
});
it("extraction: 'Pat is my manager at Acme' produces 2 entities + 3 edges", async () => {
const result = await applyExtractedEdges({
entityStore,
relationshipStore: store,
evidenceId: "utterance-pat-acme",
edges: managerOfAtCompany("PatExtract", "AcmeExtract"),
});
expect(result.entities.length).toBeGreaterThanOrEqual(3); // self + Pat + Acme
const pat = result.entities.find((e) => e.preferredName === "PatExtract");
const acme = result.entities.find((e) => e.preferredName === "AcmeExtract");
expect(pat).toBeTruthy();
expect(acme).toBeTruthy();
expect(acme?.type).toBe("organization");
expect(result.relationships).toHaveLength(3);
const types = result.relationships.map((r) => r.type).sort();
expect(types).toEqual(["managed_by", "works_at", "works_at"]);
const selfManagedBy = result.relationships.find(
(r) => r.fromEntityId === SELF_ENTITY_ID && r.type === "managed_by",
);
expect(selfManagedBy?.toEntityId).toBe(pat?.entityId);
const selfWorksAt = result.relationships.find(
(r) =>
r.fromEntityId === SELF_ENTITY_ID &&
r.type === "works_at" &&
r.toEntityId === acme?.entityId,
);
expect(selfWorksAt).toBeTruthy();
const patWorksAt = result.relationships.find(
(r) =>
r.fromEntityId === pat?.entityId &&
r.type === "works_at" &&
r.toEntityId === acme?.entityId,
);
expect(patWorksAt).toBeTruthy();
});
});