Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

271 lines
10 KiB
TypeScript

/**
* #12362 WI-7 residual — the LifeOps `dispatch_workflow` workflow step drives
* a REAL embedded workflow execution end to end.
*
* The subject under test is the seam between the two workflow layers: a
* LifeOps owner-workflow step (`kind: "dispatch_workflow"`, contributed by
* plugin-personal-assistant's default WorkflowStepRegistry pack) resolves the
* REAL `WORKFLOW_DISPATCH` service and lands a row in the REAL
* `workflow.embedded_executions` table (PGlite-backed EmbeddedWorkflowService).
*
* The step is invoked exactly the way the LifeOps dispatcher
* (`WorkflowsDomain.executeWorkflowDefinition`,
* plugin-personal-assistant/src/lifeops/domains/workflows-service.ts) invokes
* it: registry bound to the runtime → `registry.get(step.kind)` →
* `paramSchema.parse(step)` → `contribution.execute(validated, args, ctx)` —
* NOT by calling the dispatch service directly. The full `WorkflowsDomain`
* class is not importable here (its repository pulls built dists of
* plugin-browser/finances/inbox), so this file drives the registry seam the
* dispatcher consults; the loop mechanics around that seam are covered by
* plugin-personal-assistant/test/workflow-step-registry.test.ts.
*
* Cases:
* (a) dispatch_workflow step → real execution row; the parent run's
* request + accumulated outputs are threaded into the nested
* execution's triggerData (the contribution's contract)
* (b) idempotent double-fire: same payload `__idempotencyKey` collapses two
* step executions onto one embedded execution row
* (c) WORKFLOW_DISPATCH not registered → structured failure, no execution
* (d) unknown workflowId → structured failure from the real dispatch, no row
* (e) malformed step (no workflowId) fails schema validation before dispatch
*/
import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
import { registerDefaultWorkflowStepPack } from '../../../plugin-personal-assistant/src/lifeops/registries/workflow-step-default-pack.ts';
import {
createWorkflowStepRegistry,
getWorkflowStepRegistry,
registerWorkflowStepRegistry,
type WorkflowStepExecuteArgs,
type WorkflowStepExecuteContext,
} from '../../../plugin-personal-assistant/src/lifeops/registries/workflow-step-registry.ts';
import type { EmbeddedWorkflowService } from '../../src/services/embedded-workflow-service';
import {
registerWorkflowDispatchService,
WORKFLOW_DISPATCH_SERVICE_TYPE,
type WorkflowDispatchResult,
} from '../../src/services/workflow-dispatch';
import { type EmbeddedHarness, makeEmbeddedHarness } from './embedded-harness';
setDefaultTimeout(60_000);
/** A runnable two-node workflow the nested dispatch executes for real. */
async function createDispatchableWorkflow(
workflow: EmbeddedWorkflowService,
name: string
): Promise<string> {
const created = await workflow.createWorkflow({
name,
nodes: [
{
id: 'sched',
name: 'Schedule Trigger',
type: 'workflows-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [0, 0],
parameters: { intervalMs: 60_000 },
},
{
id: 'set',
name: 'Set',
type: 'workflows-nodes-base.set',
typeVersion: 3.4,
position: [200, 0],
parameters: { assignments: { assignments: [] } },
},
],
connections: {
'Schedule Trigger': { main: [[{ node: 'Set', type: 'main', index: 0 }]] },
},
});
return created.id;
}
/** The minimal owner-workflow definition the step args carry; only ownership
* defaults are read from it by contributions, none by dispatch_workflow. */
function makeLifeOpsDefinition(): WorkflowStepExecuteArgs['definition'] {
return {
id: 'lifeops-wf-1',
agentId: 'agent',
domain: 'personal',
subjectType: 'agent',
subjectId: 'agent',
visibilityScope: 'owner_only',
contextPolicy: 'default',
title: 'Nested dispatch',
triggerType: 'manual',
schedule: { kind: 'manual' },
actionPlan: { steps: [] },
permissionPolicy: {
allowBrowserActions: false,
trustedBrowserActions: false,
},
status: 'active',
createdBy: 'user',
metadata: {},
createdAt: '2026-07-04T00:00:00.000Z',
updatedAt: '2026-07-04T00:00:00.000Z',
} as unknown as WorkflowStepExecuteArgs['definition'];
}
function makeStepArgs(overrides: Partial<WorkflowStepExecuteArgs> = {}): WorkflowStepExecuteArgs {
return {
definition: makeLifeOpsDefinition(),
startedAt: new Date().toISOString(),
confirmBrowserActions: false,
request: {},
outputs: {},
previousStepValue: null,
...overrides,
};
}
/**
* Execute one step the way `WorkflowsDomain.executeWorkflowDefinition` does:
* consult the runtime-bound registry, validate against the contribution's
* schema, then execute with the run args + service context.
*/
async function executeStepViaRegistry(
h: EmbeddedHarness,
step: Record<string, unknown>,
args: WorkflowStepExecuteArgs
): Promise<unknown> {
const registry = getWorkflowStepRegistry(h.runtime);
if (!registry) throw new Error('registry not bound to runtime');
const contribution = registry.get(String(step.kind));
if (!contribution) throw new Error(`no contribution for kind ${step.kind}`);
const validated = contribution.paramSchema.parse(step);
const ctx = { runtime: h.runtime } as unknown as WorkflowStepExecuteContext;
return contribution.execute(validated, args, ctx);
}
describe('dispatch_workflow step e2e (LifeOps registry -> WORKFLOW_DISPATCH -> embedded execution)', () => {
let h: EmbeddedHarness;
beforeEach(async () => {
h = await makeEmbeddedHarness('dispatch-step-e2e-agent');
registerWorkflowDispatchService(h.runtime);
// The same wiring plugin-personal-assistant's init performs: default pack
// registered into a registry bound to this runtime.
const registry = createWorkflowStepRegistry();
registerDefaultWorkflowStepPack(registry);
registerWorkflowStepRegistry(h.runtime, registry);
});
afterEach(async () => {
await h.close();
});
test("(a) the step lands a real execution row and threads request + outputs into the nested run's triggerData", async () => {
const workflowId = await createDispatchableWorkflow(h.workflow, 'Nested digest');
const args = makeStepArgs({
request: { reason: 'parent-run' },
outputs: { calendar: { eventCount: 3 } },
});
const result = (await executeStepViaRegistry(
h,
{
kind: 'dispatch_workflow',
workflowId,
payload: { note: 'from-step' },
},
args
)) as WorkflowDispatchResult;
expect(result.ok).toBe(true);
expect(result.executionId).toBeDefined();
// Domain artifact: a real row in workflow.embedded_executions.
const { data: executions } = await h.workflow.listExecutions({
workflowId,
});
expect(executions).toHaveLength(1);
const execution = executions[0];
expect(execution.id).toBe(result.executionId as string);
expect(execution.status).toBe('success');
expect(execution.finished).toBe(true);
expect(execution.mode).toBe('trigger');
// The contribution's contract: step payload + parent request + accumulated
// outputs all arrive as the nested execution's trigger data.
const triggerData = execution.customData?.triggerData as Record<string, unknown>;
expect(triggerData).toMatchObject({
note: 'from-step',
request: { reason: 'parent-run' },
outputs: { calendar: { eventCount: 3 } },
});
});
test('(b) two step fires sharing an idempotency key collapse onto one execution row', async () => {
const workflowId = await createDispatchableWorkflow(h.workflow, 'Nested dedup');
const step = {
kind: 'dispatch_workflow',
workflowId,
// The dispatch layer's legacy payload contract: __idempotencyKey rides
// inside the payload when the caller cannot pass a second argument —
// exactly how a LifeOps step would express at-most-once semantics.
payload: { __idempotencyKey: 'step-fire-1' },
};
const first = (await executeStepViaRegistry(h, step, makeStepArgs())) as WorkflowDispatchResult;
const second = (await executeStepViaRegistry(
h,
step,
makeStepArgs()
)) as WorkflowDispatchResult;
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
expect(second.dedup).toBe(true);
expect(second.executionId).toBe(first.executionId as string);
const { data: executions } = await h.workflow.listExecutions({
workflowId,
});
expect(executions).toHaveLength(1);
});
test('(c) missing WORKFLOW_DISPATCH service yields a structured failure and no execution', async () => {
const workflowId = await createDispatchableWorkflow(h.workflow, 'Nested orphan');
h.services.delete(WORKFLOW_DISPATCH_SERVICE_TYPE);
const result = (await executeStepViaRegistry(
h,
{ kind: 'dispatch_workflow', workflowId },
makeStepArgs()
)) as { ok: boolean; error?: string };
expect(result.ok).toBe(false);
expect(result.error).toBe('WORKFLOW_DISPATCH service not registered');
const { data: executions } = await h.workflow.listExecutions({
workflowId,
});
expect(executions).toHaveLength(0);
});
test('(d) unknown workflowId is a structured failure from the real dispatch, not a throw', async () => {
const result = (await executeStepViaRegistry(
h,
{ kind: 'dispatch_workflow', workflowId: 'no-such-workflow' },
makeStepArgs()
)) as WorkflowDispatchResult;
expect(result.ok).toBe(false);
expect(result.error).toMatch(/not found/i);
const { data: executions } = await h.workflow.listExecutions({
workflowId: 'no-such-workflow',
});
expect(executions).toHaveLength(0);
});
test('(e) a step without workflowId fails schema validation before any dispatch', async () => {
// The dispatcher parses against the contribution's schema before execute;
// a malformed step must be rejected there, never reaching the service.
await expect(
executeStepViaRegistry(h, { kind: 'dispatch_workflow' }, makeStepArgs())
).rejects.toThrow();
});
});