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
251 lines
9.1 KiB
TypeScript
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([]);
|
|
});
|
|
});
|