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

201 lines
7.5 KiB
TypeScript

/**
* #12362 WI-7 residual — the WORKFLOW action's `run` op executes a workflow
* for real, keylessly (no model, no API key): action handler →
* WorkflowService.runWorkflow → EmbeddedWorkflowService → a row in
* workflow.embedded_executions.
*
* Complements the mocked-unit coverage in
* __tests__/unit/workflow-action.test.ts (op dispatch against a mocked
* service) and the live-only scenario
* packages/scenario-runner/test/scenarios/live-workflow-action-executions.scenario.ts
* (model routing): here the service stack under the action is entirely REAL —
* a real WorkflowService started via WorkflowService.start() over a real
* PGlite-backed EmbeddedWorkflowService. No workflow-service mocks.
*
* Cases:
* (a) run op → real execution row, asserted through the same real service
* layer the executions op reads (listExecutions)
* (b) a workflow whose node throws → success:false result AND a real
* error-status execution row (throwOnError:false path)
* (c) missing workflowId → structured failure, nothing executed
* (d) unknown workflowId → structured failure from the real store
* (e) validate() gates on the real service registration
*/
import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import type { HandlerCallback, HandlerOptions, IAgentRuntime, Memory } from '@elizaos/core';
import { workflowAction } from '../../src/actions/workflow';
import type { EmbeddedWorkflowService } from '../../src/services/embedded-workflow-service';
import { WORKFLOW_SERVICE_TYPE, WorkflowService } from '../../src/services/workflow-service';
import type { WorkflowExecution } from '../../src/types/index';
import { type EmbeddedHarness, makeEmbeddedHarness } from './embedded-harness';
setDefaultTimeout(60_000);
const message = { entityId: 'owner-test' } as Memory;
interface ActionHarness extends EmbeddedHarness {
service: WorkflowService;
}
async function makeActionHarness(): Promise<ActionHarness> {
const h = await makeEmbeddedHarness('workflow-action-run-agent');
// The REAL service the action resolves — started keylessly the same way the
// plugin's service lifecycle does; it adopts the registered embedded engine.
const service = await WorkflowService.start(h.runtime);
h.services.set(WORKFLOW_SERVICE_TYPE, [service]);
return { ...h, service };
}
async function runOp(
runtime: IAgentRuntime,
parameters: Record<string, unknown>,
callback?: HandlerCallback
) {
if (!workflowAction.handler) throw new Error('workflow action missing handler');
return workflowAction.handler(
runtime,
message,
undefined,
{ parameters } as HandlerOptions,
callback
);
}
/** A manual-trigger workflow; `run` executes in manual mode. */
async function createManualWorkflow(
workflow: EmbeddedWorkflowService,
name: string,
secondNode: { type: string; parameters: Record<string, unknown> }
): Promise<string> {
const created = await workflow.createWorkflow({
name,
nodes: [
{
id: 'manual',
name: 'Manual Trigger',
type: 'workflows-nodes-base.manualTrigger',
typeVersion: 1,
position: [0, 0],
parameters: {},
},
{
id: 'work',
name: 'Work',
type: secondNode.type,
typeVersion: 1,
position: [200, 0],
parameters: secondNode.parameters,
},
],
connections: {
'Manual Trigger': { main: [[{ node: 'Work', type: 'main', index: 0 }]] },
},
});
return created.id;
}
describe('WORKFLOW action run op e2e (real WorkflowService + embedded engine, keyless)', () => {
let h: ActionHarness;
beforeEach(async () => {
h = await makeActionHarness();
});
afterEach(async () => {
await h.service.stop();
await h.close();
});
test('(a) run executes the workflow and a real execution row is readable back through the service', async () => {
const workflowId = await createManualWorkflow(h.workflow, 'Run me', {
type: 'workflows-nodes-base.set',
parameters: { assignments: { assignments: [] } },
});
const callbackContents: Array<Record<string, unknown>> = [];
const callback: HandlerCallback = async (content) => {
callbackContents.push(content as Record<string, unknown>);
return [];
};
const result = await runOp(h.runtime, { action: 'run', workflowId }, callback);
expect(result.success).toBe(true);
expect(result.values?.status).toBe('success');
const executionId = result.values?.executionId as string;
expect(executionId).toBeDefined();
// The user-facing callback reported the same real execution.
expect(callbackContents).toHaveLength(1);
expect(callbackContents[0].metadata).toMatchObject({
workflowId,
executionId,
status: 'success',
});
// Domain artifact through the SAME real service layer the executions op
// uses: a finished manual-mode row in workflow.embedded_executions.
const { data: executions } = await h.service.listExecutions({ workflowId });
expect(executions).toHaveLength(1);
const execution = executions[0];
expect(execution.id).toBe(executionId);
expect(execution.status).toBe('success');
expect(execution.finished).toBe(true);
expect(execution.mode).toBe('manual');
});
test('(b) a workflow whose node throws yields success:false AND a real error-status execution row', async () => {
const workflowId = await createManualWorkflow(h.workflow, 'Run me broken', {
type: 'workflows-nodes-base.code',
parameters: { jsCode: 'throw new Error("boom")' },
});
const result = await runOp(h.runtime, { action: 'run', workflowId });
expect(result.success).toBe(false);
expect(result.values?.status).toBe('error');
// The failed run is still a persisted domain artifact (throwOnError:false
// records the error execution instead of raising).
const { data: executions } = await h.service.listExecutions({ workflowId });
expect(executions).toHaveLength(1);
expect(executions[0].status).toBe('error');
expect(executions[0].finished).toBe(true);
// The persisted diagnostics name the failing node (the embedded engine
// echoes the node failure ahead of Smithers' wrapper error).
const runError = (executions[0] as WorkflowExecution).data?.resultData?.error as
| { message?: string }
| undefined;
expect(runError?.message ?? '').toContain('Node "Work" failed');
});
test('(c) run without workflowId is a structured failure and executes nothing', async () => {
const result = await runOp(h.runtime, { action: 'run' });
expect(result.success).toBe(false);
expect(result.text).toBe('workflowId is required to run a workflow.');
const { data: executions } = await h.service.listExecutions();
expect(executions).toHaveLength(0);
});
test("(d) run with an unknown workflowId surfaces the real store's not-found failure", async () => {
const result = await runOp(h.runtime, {
action: 'run',
workflowId: 'no-such-workflow',
});
expect(result.success).toBe(false);
expect(result.text).toMatch(/not found/i);
const { data: executions } = await h.service.listExecutions();
expect(executions).toHaveLength(0);
});
test('(e) validate() reflects real service registration', async () => {
if (!workflowAction.validate) throw new Error('workflow action missing validate');
expect(await workflowAction.validate(h.runtime, message, undefined)).toBe(true);
h.services.delete(WORKFLOW_SERVICE_TYPE);
expect(await workflowAction.validate(h.runtime, message, undefined)).toBe(false);
});
});