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
255 lines
8.1 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|