Files
elizaos--eliza/plugins/plugin-personal-assistant/test/workflow-step-registry.test.ts
T
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

255 lines
8.1 KiB
TypeScript

/**
* WorkflowStepRegistry unit tests.
*
* Covers the registry contract that replaces the closed 9-branch `step.kind`
* switch in `service-mixin-workflows.ts` (audit `rigidity-hunt-audit.md`
* top-2). The dispatcher behaviour itself is integration-tested elsewhere;
* these tests verify:
* - the default pack registers all 10 built-in step kinds,
* - a synthetic third-party step kind can be registered + dispatched,
* - `UnknownWorkflowStepError` carries the offending kind + the current
* known set,
* - duplicate registration is rejected.
*/
import type { IAgentRuntime } from "@elizaos/core";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import {
__resetWorkflowStepRegistryForTests,
type AnyWorkflowStepContribution,
APP_LIFEOPS_WORKFLOW_STEP_CONTRIBUTIONS,
createWorkflowStepRegistry,
getWorkflowStepRegistry,
registerDefaultWorkflowStepPack,
registerWorkflowStepRegistry,
UnknownWorkflowStepError,
type WorkflowStepExecuteArgs,
type WorkflowStepExecuteContext,
} from "../src/lifeops/registries/index.ts";
const EXPECTED_DEFAULT_KINDS = [
"create_task",
"relock_website_access",
"resolve_website_access_callback",
"get_calendar_feed",
"get_gmail_triage",
"get_gmail_unresponded",
"get_health_summary",
"dispatch_workflow",
"summarize",
"browser",
] as const;
function makeRuntimeStub(): IAgentRuntime {
// The registry is per-runtime via WeakMap; we only need a stable object
// identity, not a working runtime.
return {} as IAgentRuntime;
}
function makeStubArgs(): WorkflowStepExecuteArgs {
return {
definition: {
id: "wf-1",
agentId: "agent",
domain: "personal",
subjectType: "agent",
subjectId: "agent",
visibilityScope: "owner_only",
contextPolicy: "default",
title: "stub",
triggerType: "manual",
schedule: { kind: "manual" },
actionPlan: { steps: [] },
permissionPolicy: {
allowBrowserActions: false,
trustedBrowserActions: false,
},
status: "active",
createdBy: "user",
metadata: {},
createdAt: "2026-05-09T00:00:00.000Z",
updatedAt: "2026-05-09T00:00:00.000Z",
} as unknown as WorkflowStepExecuteArgs["definition"],
startedAt: "2026-05-09T00:00:00.000Z",
confirmBrowserActions: false,
request: {},
outputs: {},
previousStepValue: null,
};
}
function makeStubCtx(): WorkflowStepExecuteContext {
return {} as WorkflowStepExecuteContext;
}
describe("WorkflowStepRegistry", () => {
it("default pack registers all 10 built-in step kinds", () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
const kinds = registry.list().map((c) => c.kind);
expect(kinds.sort()).toEqual([...EXPECTED_DEFAULT_KINDS].sort());
expect(APP_LIFEOPS_WORKFLOW_STEP_CONTRIBUTIONS).toHaveLength(
EXPECTED_DEFAULT_KINDS.length,
);
});
it("rejects duplicate registration", () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
const firstContribution = APP_LIFEOPS_WORKFLOW_STEP_CONTRIBUTIONS[0];
if (!firstContribution) {
throw new Error("Expected at least one workflow step contribution.");
}
expect(() => registry.register(firstContribution)).toThrow(
/already registered/,
);
});
it("registers a synthetic third-party step kind and dispatches it via execute", async () => {
const acmeStepSchema = z.object({
kind: z.literal("acme_step"),
id: z.string().optional(),
resultKey: z.string().optional(),
payload: z.object({ greeting: z.string() }),
});
type AcmeStep = z.infer<typeof acmeStepSchema>;
let received: AcmeStep | null = null;
const acmeContribution: AnyWorkflowStepContribution = {
kind: "acme_step",
describe: {
label: "Acme step",
description: "Test contribution",
provider: "test:acme",
},
paramSchema: acmeStepSchema as unknown as z.ZodType<{ kind: string }>,
async execute(step) {
received = step as AcmeStep;
return { ok: true, kind: (step as AcmeStep).kind };
},
};
const registry = createWorkflowStepRegistry();
registry.register(acmeContribution);
expect(registry.has("acme_step")).toBe(true);
const contribution = registry.get("acme_step");
expect(contribution).not.toBeNull();
const validated = contribution?.paramSchema.parse({
kind: "acme_step",
payload: { greeting: "hi" },
});
const result = await contribution?.execute(
validated,
makeStubArgs(),
makeStubCtx(),
);
expect(received).toEqual({
kind: "acme_step",
payload: { greeting: "hi" },
});
expect(result).toEqual({ ok: true, kind: "acme_step" });
});
it("UnknownWorkflowStepError carries kind + known set", () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
expect(registry.get("nonexistent")).toBeNull();
const err = new UnknownWorkflowStepError(
"nonexistent",
registry.list().map((c) => c.kind),
);
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe("UnknownWorkflowStepError");
expect(err.kind).toBe("nonexistent");
expect(err.knownKinds).toContain("create_task");
expect(err.knownKinds).toContain("browser");
expect(err.message).toContain("nonexistent");
expect(err.message).toContain("create_task");
});
it("paramSchema rejects malformed steps", () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
const relock = registry.get("relock_website_access");
expect(relock).not.toBeNull();
expect(() =>
relock?.paramSchema.parse({
kind: "relock_website_access",
request: {},
}),
).toThrow();
const ok = relock?.paramSchema.parse({
kind: "relock_website_access",
request: { groupKey: "social" },
});
expect(ok).toMatchObject({
kind: "relock_website_access",
request: { groupKey: "social" },
});
});
it("per-runtime WeakMap binding isolates registries", () => {
const r1 = makeRuntimeStub();
const r2 = makeRuntimeStub();
const reg1 = createWorkflowStepRegistry();
const reg2 = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(reg1);
registerWorkflowStepRegistry(r1, reg1);
registerWorkflowStepRegistry(r2, reg2);
expect(getWorkflowStepRegistry(r1)?.list()).toHaveLength(
EXPECTED_DEFAULT_KINDS.length,
);
expect(getWorkflowStepRegistry(r2)?.list()).toHaveLength(0);
__resetWorkflowStepRegistryForTests(r1);
expect(getWorkflowStepRegistry(r1)).toBeNull();
expect(getWorkflowStepRegistry(r2)).not.toBeNull();
});
it("default summarize contribution describes previousStepValue when no sourceKey is set", async () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
const summarize = registry.get("summarize");
expect(summarize).not.toBeNull();
const args: WorkflowStepExecuteArgs = {
...makeStubArgs(),
previousStepValue: { count: 7 },
};
const validated = summarize?.paramSchema.parse({
kind: "summarize",
prompt: "stat",
});
const result = (await summarize?.execute(
validated,
args,
makeStubCtx(),
)) as { text: string };
expect(typeof result.text).toBe("string");
expect(result.text.length).toBeGreaterThan(0);
});
it("default browser contribution short-circuits when permissionPolicy.allowBrowserActions=false", async () => {
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
const browser = registry.get("browser");
expect(browser).not.toBeNull();
const validated = browser?.paramSchema.parse({
kind: "browser",
sessionTitle: "noop",
actions: [{ kind: "open" }],
});
const result = await browser?.execute(
validated,
makeStubArgs(),
makeStubCtx(),
);
expect(result).toEqual({
blocked: true,
reason: "browser_actions_disabled",
});
});
});