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

251 lines
9.1 KiB
TypeScript

// Exercises LifeOps owner workflows, connector boundaries, and scheduled-task behavior.
import type { Action, Plugin, Service } from "@elizaos/core";
import { blockerPlugin } from "@elizaos/plugin-blocker";
import { calendarPlugin } from "@elizaos/plugin-calendar";
import { financesPlugin } from "@elizaos/plugin-finances";
import { goalsPlugin } from "@elizaos/plugin-goals";
import { healthPlugin } from "@elizaos/plugin-health";
import { inboxPlugin } from "@elizaos/plugin-inbox";
import { remoteDesktopPlugin } from "@elizaos/plugin-remote-desktop";
import { describe, expect, it } from "vitest";
import { personalAssistantPlugin } from "../src/plugin.ts";
/**
* Integration invariant for the LifeOps decomposition.
*
* After breaking the monolith into focused plugins, PA (the chief-of-staff hub)
* is composed *with* the seven domain plugins it integrates. This test pins the
* properties that a large decomposition silently breaks: a dropped owner action,
* two different service classes claiming one serviceType (the runtime skips the
* second, so it never starts), two views shadowing one id, or an affinity-map
* action name that no loaded plugin actually registers.
*
* It models the runtime's real registration behavior (first-wins dedup —
* `AgentRuntime.registerAction`/`registerService` skip a name/serviceType that
* is already present) without booting the scheduler stack, so it is fast and
* deterministic.
*/
// The plugins PA integrates. calendar/finances/inbox/remote-desktop/goals/health
// are auto-registered by PA.init(); calendar/goals are registered in that
// topology without their standalone action arrays so their scaffold/standalone
// parents cannot shadow PA's richer owner-operation umbrellas. Health registers
// views and registry contributions only; PA owns the host-adapted owner actions.
// blocker is a standalone PA-free topology surface that PA reuses by direct
// import. The union below is the full composed surface.
const SUBPLUGINS: Plugin[] = [
calendarPlugin,
financesPlugin,
inboxPlugin,
remoteDesktopPlugin,
goalsPlugin,
healthPlugin,
blockerPlugin,
];
// Runtime registerPlugin() runs init() before processing the plugin's actions,
// so PA auto-dependencies are registered first. Model the PA topology by
// stripping action arrays from dependencies whose canonical parent actions
// remain PA-owned.
function withoutActions(plugin: Plugin): Plugin {
return { ...plugin, actions: [] };
}
const AUTO_REGISTERED: Plugin[] = [
withoutActions(calendarPlugin),
financesPlugin,
inboxPlugin,
remoteDesktopPlugin,
withoutActions(goalsPlugin),
healthPlugin,
];
const ALL: Plugin[] = [personalAssistantPlugin, ...SUBPLUGINS];
type ServiceClass = typeof Service & { serviceType?: string };
function actionsOf(plugin: Plugin): Action[] {
return (plugin.actions ?? []) as Action[];
}
function servicesOf(plugin: Plugin): ServiceClass[] {
return (plugin.services ?? []) as unknown as ServiceClass[];
}
/** Runtime first-wins: the first plugin to register a name owns it. */
function firstWinsActionOwners(
plugins: Plugin[],
): Map<string, { plugin: string; action: Action }> {
const reg = new Map<string, { plugin: string; action: Action }>();
for (const plugin of plugins) {
for (const action of actionsOf(plugin)) {
if (!reg.has(action.name)) {
reg.set(action.name, { plugin: plugin.name, action });
}
}
}
return reg;
}
describe("LifeOps decomposition — composed plugin surface", () => {
it("every action across the composed set is structurally valid", () => {
for (const plugin of ALL) {
for (const action of actionsOf(plugin)) {
expect(
typeof action.name === "string" && action.name.length > 0,
`${plugin.name} has an action with no name`,
).toBe(true);
expect(
typeof action.handler === "function",
`${plugin.name}:${action.name} has no handler`,
).toBe(true);
}
}
});
it("no serviceType is claimed by two different service classes", () => {
// Same serviceType from the same class (a re-export) is fine; two *different*
// classes on one serviceType means the runtime silently never starts one.
const byType = new Map<string, Set<ServiceClass>>();
for (const plugin of ALL) {
for (const svc of servicesOf(plugin)) {
const type = svc.serviceType;
if (!type) continue;
const set = byType.get(type) ?? new Set<ServiceClass>();
set.add(svc);
byType.set(type, set);
}
}
const collisions = [...byType.entries()]
.filter(([, classes]) => classes.size > 1)
.map(([type]) => type);
expect(
collisions,
`serviceType claimed by 2+ classes: ${collisions}`,
).toEqual([]);
});
it("no (view id + surface) is registered by two plugins (no shell shadowing)", () => {
// The same id across future modalities is a legitimate variant pattern; a
// real shadow is two *plugins* claiming the same id+surface, which the
// shell renders ambiguously. (PA no longer registers a view — the LifeOps
// overview was removed — so this asserts the per-domain plugin views stay
// unshadowed.)
const byKey = new Map<string, Set<string>>();
for (const plugin of ALL) {
for (const view of plugin.views ?? []) {
const key = `${view.id}::${(view as { viewType?: string }).viewType ?? "gui"}`;
const owners = byKey.get(key) ?? new Set<string>();
owners.add(plugin.name);
byKey.set(key, owners);
}
}
const shadowed = [...byKey.entries()]
.filter(([, owners]) => owners.size > 1)
.map(([key, owners]) => [key, [...owners]]);
expect(
shadowed,
`view id+surface claimed by 2+ plugins: ${JSON.stringify(shadowed)}`,
).toEqual([]);
});
it("the decomposition did not drop any owner action surface", () => {
// Names that must exist somewhere in the composed set (umbrella action
// names, post-promotion). If any is missing, a surface was lost in a move.
const EXPECTED_OWNER_ACTIONS = [
"INBOX",
"CALENDAR",
"CONFLICT_DETECT",
"OWNER_FINANCES",
"OWNER_GOALS",
"OWNER_ROUTINES",
"OWNER_REMINDERS",
"OWNER_ALARMS",
"OWNER_TODOS",
"OWNER_HEALTH",
"OWNER_DOCUMENTS",
"PERSONAL_ASSISTANT",
"BRIEF",
"PRIORITIZE",
"RESOLVE_REQUEST",
"CREDENTIALS",
"SCHEDULED_TASKS",
"VOICE_CALL",
"CONNECTOR",
"ENTITY",
"WORK_THREAD",
"REMOTE_DESKTOP",
];
const present = new Set(
ALL.flatMap((p) => actionsOf(p).map((a) => a.name)),
);
const missing = EXPECTED_OWNER_ACTIONS.filter((n) => !present.has(n));
expect(
missing,
`owner actions absent from the composed set: ${missing}`,
).toEqual([]);
});
it("PA's umbrella wins the auto-registered overlaps (first-wins ordering)", () => {
const reg = firstWinsActionOwners([
...AUTO_REGISTERED,
personalAssistantPlugin,
]);
for (const name of [
"CALENDAR",
"CONFLICT_DETECT",
"OWNER_GOALS",
"OWNER_ROUTINES",
"OWNER_REMINDERS",
"OWNER_ALARMS",
]) {
expect(reg.get(name)?.plugin, `${name} winner`).toBe(
personalAssistantPlugin.name,
);
}
});
it("lifeops view→action affinity names all resolve in the composed set", () => {
// Mirrors the lifeops slice of VIEW_ACTION_MAP
// (packages/agent/src/runtime/view-action-affinity.ts). Inlined to keep this
// test free of a cross-package import; the agent drift test guards the names
// themselves, this guards that a loaded plugin actually registers them.
// OWNER_SCREENTIME is a Darwin-only umbrella: its only end-to-end data
// source (the native activity tracker) is macOS-only, so plugin.ts gates it
// behind `isDarwin()` (platformGatedActionUmbrellas). The affinity map lists
// it unconditionally for the planner, but it only *registers* as an action
// on darwin hosts — so only require it to resolve there.
const isDarwinHost = process.platform === "darwin";
const LIFEOPS_VIEW_ACTIONS: Record<string, string[]> = {
calendar: ["CALENDAR", "CONFLICT_DETECT"],
health: isDarwinHost
? ["OWNER_HEALTH", "OWNER_SCREENTIME"]
: ["OWNER_HEALTH"],
// The focus view's domain action is the BLOCK umbrella (list_active /
// release are now subactions of it, not standalone actions).
focus: ["BLOCK"],
finances: ["OWNER_FINANCES"],
inbox: ["INBOX"],
goals: [
"OWNER_GOALS",
"OWNER_ALARMS",
"OWNER_REMINDERS",
"OWNER_ROUTINES",
],
todos: ["OWNER_TODOS"],
lifeops: ["PERSONAL_ASSISTANT"],
};
const present = new Set(
ALL.flatMap((p) => actionsOf(p).map((a) => a.name)),
);
const unresolved: string[] = [];
for (const [view, names] of Object.entries(LIFEOPS_VIEW_ACTIONS)) {
for (const name of names) {
if (!present.has(name)) unresolved.push(`${view}:${name}`);
}
}
expect(
unresolved,
`affinity action names with no registering plugin: ${unresolved}`,
).toEqual([]);
});
});