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

345 lines
12 KiB
TypeScript

/**
* Regression coverage for model-mediated scheduled-task dispatch: a task's
* instruction-voice `promptInstructions` must never reach a user-visible
* surface (assistant stream, notification body, connector channel send)
* verbatim — the dispatcher renders it through the model, and a render failure
* is a typed retryable dispatch failure, never a raw-instruction fallback.
* Deterministic: the model is stubbed at the runtime boundary (`useModel`).
*/
import type { IAgentRuntime } from "@elizaos/core";
import {
buildScheduledDispatchRenderPrompt,
buildScheduledDispatchTitlePrompt,
} from "@elizaos/plugin-scheduling";
import { beforeEach, describe, expect, it } from "vitest";
import type { ChannelContribution } from "../src/lifeops/channels/contract.js";
import {
createChannelRegistry,
registerChannelRegistry,
} from "../src/lifeops/channels/index.js";
import { createProductionScheduledTaskDispatcher } from "../src/lifeops/scheduled-task/runtime-wiring.js";
import {
enableAgentEventServiceStub,
getAgentEventServiceStubEvents,
resetAgentBackupStubState,
resetAgentEventServiceStub,
setAgentBackupStubState,
} from "./stubs/agent.ts";
const INSTRUCTION =
"Remind the owner to take their medication and ask how they slept.";
const RENDERED = "Time for your medication — and how did you sleep last night?";
const RENDERED_TITLE = "Medication and sleep check";
interface ReportedErrorCapture {
scope: string;
error: unknown;
context?: Record<string, unknown>;
}
function makeRuntime(opts: {
model?: (params: { prompt: string }) => string | Promise<string>;
notifier?: { notify: (input: Record<string, unknown>) => Promise<unknown> };
}) {
const modelPrompts: string[] = [];
const reported: ReportedErrorCapture[] = [];
const runtime = {
agentId: "00000000-0000-0000-0000-0000000000aa",
getService: (type: string) =>
type === "notification" ? (opts.notifier ?? null) : null,
getSetting: () => null,
...(opts.model
? {
useModel: async (_type: string, params: { prompt: string }) => {
modelPrompts.push(params.prompt);
return opts.model?.(params);
},
}
: {}),
reportError: (
scope: string,
error: unknown,
context?: Record<string, unknown>,
) => {
reported.push({ scope, error, ...(context ? { context } : {}) });
},
} as unknown as IAgentRuntime;
return { runtime, modelPrompts, reported };
}
function inAppRecord(overrides: Record<string, unknown> = {}) {
return {
taskId: "st_render_test",
firedAtIso: "2026-07-05T09:00:00.000Z",
channelKey: "in_app",
intensity: "normal" as const,
promptInstructions: INSTRUCTION,
contextRequest: undefined,
output: undefined,
metadata: undefined,
...overrides,
};
}
function sendCapableChannel(
send: ChannelContribution["send"],
): ChannelContribution {
return {
kind: "telegram",
describe: { label: "Telegram" },
capabilities: {
send: true,
read: true,
reminders: true,
voice: false,
attachments: true,
quietHoursAware: true,
},
send,
};
}
beforeEach(() => {
resetAgentEventServiceStub();
resetAgentBackupStubState();
});
describe("scheduled dispatch renders promptInstructions through the model", () => {
it("delivers model output — never raw or generic copy — to the assistant stream and notification", async () => {
enableAgentEventServiceStub();
const notified: Record<string, unknown>[] = [];
const { runtime, modelPrompts } = makeRuntime({
model: ({ prompt }) =>
prompt.includes("notification title") ? RENDERED_TITLE : RENDERED,
notifier: {
notify: async (input) => {
notified.push(input);
return { id: "n1" };
},
},
});
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(inAppRecord());
expect(result).toMatchObject({ ok: true });
// The instruction fed the model as prompt payload...
expect(modelPrompts).toHaveLength(2);
expect(modelPrompts[0]).toContain(INSTRUCTION);
expect(modelPrompts[1]).toContain(RENDERED);
expect(modelPrompts[1]).not.toContain(INSTRUCTION);
// ...and only the model's rendering reached the user-visible surfaces.
const events = getAgentEventServiceStubEvents();
expect(events).toHaveLength(1);
expect(events[0]?.data.text).toBe(RENDERED);
expect(String(events[0]?.data.text)).not.toContain(
"Remind the owner to take",
);
expect(notified).toHaveLength(1);
expect(notified[0]?.title).toBe(RENDERED_TITLE);
expect(notified[0]?.title).not.toBe("Reminder");
expect(notified[0]?.title).not.toBe("Approval needed");
expect(notified[0]?.body).toBe(RENDERED);
expect(String(notified[0]?.body)).not.toContain("Remind the owner to take");
});
it("delivers the model output — never the raw instruction — to a connector channel send", async () => {
const sent: Array<{ message?: unknown }> = [];
const { runtime, modelPrompts } = makeRuntime({ model: () => RENDERED });
const registry = createChannelRegistry();
registry.register(
sendCapableChannel(async (payload) => {
sent.push(payload as { message?: unknown });
return { ok: true, messageId: "m1" };
}),
);
registerChannelRegistry(runtime, registry);
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(
inAppRecord({
channelKey: "telegram",
output: { destination: "channel", target: "telegram:owner-dm" },
}),
);
expect(result).toMatchObject({ ok: true, messageId: "m1" });
expect(modelPrompts).toHaveLength(1);
expect(sent).toHaveLength(1);
expect(sent[0]?.message).toBe(RENDERED);
expect(sent[0]?.message).not.toBe(INSTRUCTION);
});
it("a throwing model is a typed retryable failure with reportError — nothing is emitted, no raw fallback", async () => {
enableAgentEventServiceStub();
const notified: Record<string, unknown>[] = [];
const { runtime, reported } = makeRuntime({
model: () => {
throw new Error("model backend down");
},
notifier: {
notify: async (input) => {
notified.push(input);
return { id: "n1" };
},
},
});
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(inAppRecord());
expect(result).toMatchObject({
ok: false,
reason: "transport_error",
userActionable: false,
retryAfterMinutes: 5,
});
expect(getAgentEventServiceStubEvents()).toHaveLength(0);
expect(notified).toHaveLength(0);
expect(reported).toHaveLength(1);
expect(reported[0]?.scope).toBe("lifeops:scheduled-task:dispatch-render");
});
it("a runtime without a model surface fails closed instead of sending the raw instruction", async () => {
enableAgentEventServiceStub();
const { runtime, reported } = makeRuntime({});
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(inAppRecord());
expect(result).toMatchObject({
ok: false,
reason: "transport_error",
retryAfterMinutes: 5,
});
expect(getAgentEventServiceStubEvents()).toHaveLength(0);
expect(reported).toHaveLength(1);
});
it("blank model output fails closed instead of sending the raw instruction", async () => {
enableAgentEventServiceStub();
const { runtime, reported } = makeRuntime({ model: () => " \n" });
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(inAppRecord());
expect(result).toMatchObject({ ok: false, reason: "transport_error" });
expect(getAgentEventServiceStubEvents()).toHaveLength(0);
expect(reported).toHaveLength(1);
});
it("a channel-send failure never leaks the raw instruction and preserves the typed result", async () => {
const { runtime } = makeRuntime({ model: () => RENDERED });
const registry = createChannelRegistry();
registry.register(
sendCapableChannel(async () => ({
ok: false as const,
reason: "rate_limited" as const,
retryAfterMinutes: 8,
userActionable: false,
})),
);
registerChannelRegistry(runtime, registry);
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(
inAppRecord({
channelKey: "telegram",
output: { destination: "channel", target: "telegram:owner-dm" },
}),
);
expect(result).toMatchObject({
ok: false,
reason: "rate_limited",
retryAfterMinutes: 8,
});
});
it("structural system operations (local backup) never touch the model", async () => {
setAgentBackupStubState({
createdBackup: {
fileName: "2026-07-05T090000Z.agent-backup.json",
path: "/tmp/2026-07-05T090000Z.agent-backup.json",
createdAt: "2026-07-05T09:00:00.000Z",
agentId: "00000000-0000-0000-0000-0000000000aa",
stateSha256: "abc123",
sizeBytes: 4096,
},
});
const { runtime, modelPrompts } = makeRuntime({ model: () => RENDERED });
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(
inAppRecord({
metadata: {
systemOperation: "agent.localBackup",
backupTarget: "local-file",
},
}),
);
expect(result).toMatchObject({ ok: true });
expect(modelPrompts).toHaveLength(0);
});
it("an unconnected channel fails before rendering — no wasted model call", async () => {
const { runtime, modelPrompts } = makeRuntime({ model: () => RENDERED });
registerChannelRegistry(runtime, createChannelRegistry());
const dispatcher = createProductionScheduledTaskDispatcher({ runtime });
const result = await dispatcher.dispatch(
inAppRecord({
channelKey: "missing",
output: { destination: "channel", target: "missing:owner" },
}),
);
expect(result).toMatchObject({ ok: false, reason: "disconnected" });
expect(modelPrompts).toHaveLength(0);
});
});
describe("buildScheduledDispatchRenderPrompt", () => {
it("embeds the instruction as opaque payload with delivery framing", () => {
const prompt = buildScheduledDispatchRenderPrompt({
promptInstructions: INSTRUCTION,
intensity: "normal",
firedAtIso: "2026-07-05T09:00:00.000Z",
});
expect(prompt).toContain(INSTRUCTION);
expect(prompt).toContain("not the message itself");
expect(prompt).toContain("Fired at: 2026-07-05T09:00:00.000Z");
});
it("keys urgency framing on the structural intensity field", () => {
const urgent = buildScheduledDispatchRenderPrompt({
promptInstructions: INSTRUCTION,
intensity: "urgent",
firedAtIso: "2026-07-05T09:00:00.000Z",
});
expect(urgent).toContain("urgent");
const soft = buildScheduledDispatchRenderPrompt({
promptInstructions: INSTRUCTION,
intensity: "soft",
firedAtIso: "2026-07-05T09:00:00.000Z",
});
expect(soft).toContain("gentle");
});
});
describe("buildScheduledDispatchTitlePrompt", () => {
it("uses the rendered body, not the instruction payload, as notification title context", () => {
const prompt = buildScheduledDispatchTitlePrompt(
{
intensity: "normal",
firedAtIso: "2026-07-05T09:00:00.000Z",
},
RENDERED,
);
expect(prompt).toContain(RENDERED);
expect(prompt).toContain("under 8 words");
expect(prompt).not.toContain(INSTRUCTION);
});
});