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
180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
/** Unit tests for WORKFLOW_DISPATCH service creation, registration, and dispatch (deterministic, mocked core). */
|
|
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
import * as actualCore from '@elizaos/core';
|
|
import { logger } from '@elizaos/core';
|
|
import { EMBEDDED_WORKFLOW_SERVICE_TYPE } from '../../src/services/embedded-workflow-service';
|
|
import {
|
|
createWorkflowDispatchService,
|
|
registerWorkflowDispatchService,
|
|
WORKFLOW_DISPATCH_SERVICE_TYPE,
|
|
} from '../../src/services/workflow-dispatch';
|
|
|
|
// `mock.module` replaces the module globally for the rest of the bun-test run,
|
|
// so preserve every real `@elizaos/core` export and swap in a complete spy
|
|
// logger. A partial logger here would strip `.info`/`.error` (and `Service`)
|
|
// from every test file loaded afterward.
|
|
mock.module('@elizaos/core', () => ({
|
|
...actualCore,
|
|
logger: {
|
|
trace: mock(() => {}),
|
|
debug: mock(() => {}),
|
|
info: mock(() => {}),
|
|
warn: mock(() => {}),
|
|
error: mock(() => {}),
|
|
fatal: mock(() => {}),
|
|
child: mock(() => logger),
|
|
},
|
|
}));
|
|
|
|
type FakeExecution = { id?: string };
|
|
|
|
function makeRuntime(service: unknown = null) {
|
|
const services = new Map<string, unknown>();
|
|
return {
|
|
services,
|
|
getService: mock((type: string) => (type === EMBEDDED_WORKFLOW_SERVICE_TYPE ? service : null)),
|
|
};
|
|
}
|
|
|
|
function makeEmbeddedService() {
|
|
return {
|
|
executeWorkflow: mock(
|
|
async (
|
|
workflowId: string,
|
|
options: {
|
|
mode: string;
|
|
triggerData: Record<string, unknown>;
|
|
idempotencyKey?: string;
|
|
}
|
|
): Promise<FakeExecution> => ({
|
|
id: `${workflowId}:${options.idempotencyKey ?? 'fresh'}`,
|
|
})
|
|
),
|
|
findExecutionByIdempotencyKey: mock(
|
|
async (_workflowId: string, _idempotencyKey: string) => null as FakeExecution | null
|
|
),
|
|
};
|
|
}
|
|
|
|
describe('workflow dispatch service', () => {
|
|
beforeEach(() => {
|
|
(logger.warn as ReturnType<typeof mock>).mockClear();
|
|
});
|
|
|
|
it('rejects blank workflow ids before consulting the runtime', async () => {
|
|
const runtime = makeRuntime();
|
|
const dispatch = createWorkflowDispatchService(runtime as never);
|
|
|
|
await expect(dispatch.execute(' ')).resolves.toEqual({
|
|
ok: false,
|
|
error: 'workflow id required',
|
|
});
|
|
expect(runtime.getService).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns a clear error when the embedded workflow service is absent', async () => {
|
|
const dispatch = createWorkflowDispatchService(makeRuntime() as never);
|
|
|
|
await expect(dispatch.execute('wf-1')).resolves.toEqual({
|
|
ok: false,
|
|
error: 'embedded workflow service not registered',
|
|
});
|
|
});
|
|
|
|
it('delegates to executeWorkflow with stripped payload idempotency keys', async () => {
|
|
const embedded = makeEmbeddedService();
|
|
const dispatch = createWorkflowDispatchService(makeRuntime(embedded) as never);
|
|
|
|
await expect(
|
|
dispatch.execute(' wf-1 ', {
|
|
__idempotencyKey: 'tick-1',
|
|
source: 'schedule',
|
|
})
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
executionId: 'wf-1:tick-1',
|
|
});
|
|
expect(embedded.findExecutionByIdempotencyKey).toHaveBeenCalledWith('wf-1', 'tick-1');
|
|
expect(embedded.executeWorkflow).toHaveBeenCalledWith('wf-1', {
|
|
mode: 'trigger',
|
|
triggerData: { source: 'schedule' },
|
|
idempotencyKey: 'tick-1',
|
|
});
|
|
});
|
|
|
|
it('returns a dedup result for an existing idempotency row', async () => {
|
|
const embedded = makeEmbeddedService();
|
|
embedded.findExecutionByIdempotencyKey.mockImplementation(async () => ({
|
|
id: 'existing-execution',
|
|
}));
|
|
const dispatch = createWorkflowDispatchService(makeRuntime(embedded) as never);
|
|
|
|
await expect(dispatch.execute('wf-1', {}, { idempotencyKey: 'tick-1' })).resolves.toEqual({
|
|
ok: true,
|
|
executionId: 'existing-execution',
|
|
dedup: true,
|
|
});
|
|
expect(embedded.executeWorkflow).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('collapses concurrent dispatches with the same idempotency key', async () => {
|
|
let release!: () => void;
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const embedded = makeEmbeddedService();
|
|
embedded.executeWorkflow.mockImplementation(async () => {
|
|
await gate;
|
|
return { id: 'execution-1' };
|
|
});
|
|
const dispatch = createWorkflowDispatchService(makeRuntime(embedded) as never);
|
|
|
|
const first = dispatch.execute('wf-1', {}, { idempotencyKey: 'tick-1' });
|
|
const second = dispatch.execute('wf-1', {}, { idempotencyKey: 'tick-1' });
|
|
release();
|
|
|
|
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
{ ok: true, executionId: 'execution-1' },
|
|
{ ok: true, executionId: 'execution-1', dedup: true },
|
|
]);
|
|
expect(embedded.executeWorkflow).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('wraps execution failures without throwing', async () => {
|
|
const embedded = makeEmbeddedService();
|
|
embedded.executeWorkflow.mockImplementation(async () => {
|
|
throw new Error('engine offline');
|
|
});
|
|
const dispatch = createWorkflowDispatchService(makeRuntime(embedded) as never);
|
|
|
|
await expect(dispatch.execute('wf-1')).resolves.toEqual({
|
|
ok: false,
|
|
error: 'engine offline',
|
|
});
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
{ src: 'plugin:workflow:dispatch' },
|
|
'Workflow execution failed for wf-1: engine offline'
|
|
);
|
|
});
|
|
|
|
it('registers a stoppable service entry in the runtime services map', async () => {
|
|
const runtime = makeRuntime(makeEmbeddedService());
|
|
|
|
registerWorkflowDispatchService(runtime as never);
|
|
|
|
const entries = runtime.services.get(WORKFLOW_DISPATCH_SERVICE_TYPE);
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries?.[0]).toEqual(
|
|
expect.objectContaining({
|
|
capabilityDescription:
|
|
'Executes embedded workflows by id via the in-process workflow service.',
|
|
})
|
|
);
|
|
await expect(entries?.[0].execute('wf-1')).resolves.toEqual({
|
|
ok: true,
|
|
executionId: 'wf-1:fresh',
|
|
});
|
|
await expect(entries?.[0].stop()).resolves.toBeUndefined();
|
|
});
|
|
});
|