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

320 lines
10 KiB
TypeScript

/**
* Unit tests for the Discord DM sensitive-request adapter — DM delivery of
* approval requests, against a mocked runtime and Discord client.
*/
import type { IAgentRuntime, SensitiveRequest } from "@elizaos/core";
import {
createSensitiveRequestDispatchRegistry,
type SensitiveRequestDeliveryAdapter,
} from "@elizaos/core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createDiscordDmSensitiveRequestAdapter,
discordDmSensitiveRequestAdapter,
} from "../sensitive-request-adapter";
interface MockDmChannel {
id: string;
send: ReturnType<typeof vi.fn>;
}
interface MockUser {
createDM: ReturnType<typeof vi.fn>;
}
function makeMockDiscord(
dmId = "dm-channel-123",
throwOn?: "fetch" | "createDM" | "send",
) {
const dmChannel: MockDmChannel = {
id: dmId,
send: vi.fn(async () => ({ id: "msg-1" })),
};
if (throwOn === "send") {
dmChannel.send = vi.fn(async () => {
throw new Error("send failed");
});
}
const user: MockUser = {
createDM: vi.fn(async () => dmChannel),
};
if (throwOn === "createDM") {
user.createDM = vi.fn(async () => {
throw new Error("createDM failed");
});
}
const fetch = vi.fn(async () => user);
const fetchThrowing = vi.fn(async () => {
throw new Error("fetch failed");
});
const client = {
users: { fetch: throwOn === "fetch" ? fetchThrowing : fetch },
};
return { client, user, dmChannel };
}
function makeRuntime(overrides: Partial<IAgentRuntime> = {}): IAgentRuntime {
return {
getSetting: vi.fn(() => undefined),
getService: vi.fn(() => null),
...overrides,
} as unknown as IAgentRuntime;
}
function makeRequest(
overrides: Partial<SensitiveRequest> = {},
): SensitiveRequest {
return {
id: "req-1",
kind: "secret",
status: "pending",
agentId: "agent-1",
requesterEntityId: "user-snowflake-9999",
target: { kind: "secret", key: "OPENAI_API_KEY" },
policy: {
actor: "owner_or_linked_identity",
requirePrivateDelivery: true,
requireAuthenticatedLink: true,
allowInlineOwnerAppEntry: true,
allowPublicLink: false,
allowDmFallback: true,
allowTunnelLink: true,
allowCloudLink: true,
},
delivery: {
kind: "secret",
source: "dm",
mode: "private_dm",
policy: {
actor: "owner_or_linked_identity",
requirePrivateDelivery: true,
requireAuthenticatedLink: true,
allowInlineOwnerAppEntry: true,
allowPublicLink: false,
allowDmFallback: true,
allowTunnelLink: true,
allowCloudLink: true,
},
privateRouteRequired: true,
publicLinkAllowed: false,
authenticated: false,
canCollectValueInCurrentChannel: true,
reason: "current channel is private",
instruction: "Provide the secret in this DM.",
},
expiresAt: "2099-01-01T00:00:00.000Z",
createdAt: "2026-05-10T00:00:00.000Z",
updatedAt: "2026-05-10T00:00:00.000Z",
...overrides,
} as SensitiveRequest;
}
describe("discordDmSensitiveRequestAdapter", () => {
let mockDiscord: ReturnType<typeof makeMockDiscord>;
let runtime: IAgentRuntime;
beforeEach(() => {
mockDiscord = makeMockDiscord();
runtime = makeRuntime();
});
it("declares target 'dm'", () => {
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: mockDiscord.client }) as never,
});
expect(adapter.target).toBe("dm");
});
it("happy path: sends a DM and returns delivered=true with channel id and expiresAt", async () => {
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: mockDiscord.client }) as never,
});
const request = makeRequest();
const result = await adapter.deliver({
request,
channelId: "user-snowflake-1234",
runtime,
});
expect(result).toEqual({
delivered: true,
target: "dm",
channelId: "dm-channel-123",
expiresAt: request.expiresAt,
});
expect(mockDiscord.client.users.fetch).toHaveBeenCalledWith(
"user-snowflake-1234",
);
expect(mockDiscord.user.createDM).toHaveBeenCalledTimes(1);
expect(mockDiscord.dmChannel.send).toHaveBeenCalledTimes(1);
const sendArg = mockDiscord.dmChannel.send.mock.calls[0]?.[0] as {
content: string;
};
expect(sendArg.content).toContain(request.delivery.instruction);
expect(sendArg.content).toContain(request.expiresAt);
});
it("falls back to request.requesterEntityId when channelId is not provided", async () => {
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: mockDiscord.client }) as never,
});
const request = makeRequest({ requesterEntityId: "user-snowflake-9999" });
const result = await adapter.deliver({ request, runtime });
expect(result.delivered).toBe(true);
expect(mockDiscord.client.users.fetch).toHaveBeenCalledWith(
"user-snowflake-9999",
);
});
it("returns delivered=false with error when neither channelId nor requesterEntityId is set", async () => {
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: mockDiscord.client }) as never,
});
const request = makeRequest({ requesterEntityId: null });
const result = await adapter.deliver({ request, runtime });
expect(result.delivered).toBe(false);
expect(result.error).toMatch(/no discord user id/i);
expect(mockDiscord.client.users.fetch).not.toHaveBeenCalled();
});
it("returns delivered=false with 'Discord service unavailable' when service is missing", async () => {
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => null,
});
const result = await adapter.deliver({
request: makeRequest(),
channelId: "user-1",
runtime,
});
expect(result).toEqual({
delivered: false,
target: "dm",
error: "Discord service unavailable",
});
});
it("failure path: surfaces error message when send throws", async () => {
const failing = makeMockDiscord("dm-x", "send");
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: failing.client }) as never,
});
const result = await adapter.deliver({
request: makeRequest(),
channelId: "user-1",
runtime,
});
expect(result.delivered).toBe(false);
expect(result.error).toBe("send failed");
});
it("failure path: surfaces error when users.fetch throws", async () => {
const failing = makeMockDiscord("dm-x", "fetch");
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: failing.client }) as never,
});
const result = await adapter.deliver({
request: makeRequest(),
channelId: "user-1",
runtime,
});
expect(result.delivered).toBe(false);
expect(result.error).toBe("fetch failed");
});
it("includes the cloud-hosted link when kind=secret and cloud is paired", async () => {
const cloudRuntime = makeRuntime({
getSetting: vi.fn((key: string) =>
key === "ELIZA_CLOUD_API_KEY" ? "cloud-key-abc" : undefined,
) as IAgentRuntime["getSetting"],
});
const adapter = createDiscordDmSensitiveRequestAdapter({
getDiscordService: () => ({ client: mockDiscord.client }) as never,
});
const request = makeRequest({
delivery: {
...makeRequest().delivery,
mode: "cloud_authenticated_link",
linkBaseUrl: "https://cloud.eliza.example/secret/req-1",
},
});
const result = await adapter.deliver({
request,
channelId: "user-1",
runtime: cloudRuntime,
});
expect(result.delivered).toBe(true);
const sent = mockDiscord.dmChannel.send.mock.calls[0]?.[0] as {
content: string;
};
expect(sent.content).toContain("https://cloud.eliza.example/secret/req-1");
});
});
// The connector leg of #14326: the REAL exported adapter, wired through the REAL
// core dispatch registry the way production loads it, proving (a) two connectors
// sharing the "dm" target resolve per channel and (b) the secret value never
// appears in the outbound DM the connector puts on the wire — only the link does.
describe("discordDmSensitiveRequestAdapter — real dispatch-registry integration (#14326)", () => {
const HOSTED_LINK = "https://cloud.eliza.example/sensitive-requests/req-1";
const SECRET_SENTINEL = "sk-live-DO-NOT-LEAK-discord-1234567890";
function makeCloudRuntimeWithDiscord(client: unknown): IAgentRuntime {
return {
getSetting: vi.fn((key: string) =>
key === "ELIZA_CLOUD_API_KEY" ? "cloud-key-abc" : undefined,
),
getService: vi.fn((name: string) =>
name === "discord" ? { client } : null,
),
} as unknown as IAgentRuntime;
}
it("resolve('dm') picks Discord over a Telegram-like sibling when the Discord service is live", () => {
const registry = createSensitiveRequestDispatchRegistry();
// A sibling that claims the channel only when a "telegram" service exists —
// here it does not, so the registry must fall through to Discord.
const telegramLike: SensitiveRequestDeliveryAdapter = {
target: "dm",
supportsChannel: (_ch, runtime) =>
Boolean(
(runtime as { getService?: (n: string) => unknown })?.getService?.(
"telegram",
),
),
deliver: async () => ({ delivered: true, target: "dm" }),
};
registry.register(telegramLike);
registry.register(discordDmSensitiveRequestAdapter);
const runtime = makeCloudRuntimeWithDiscord(makeMockDiscord().client);
expect(registry.resolve?.("dm", "user-snowflake-1234", runtime)).toBe(
discordDmSensitiveRequestAdapter,
);
});
it("delivers the hosted link in the DM but never the secret value", async () => {
const mock = makeMockDiscord();
const runtime = makeCloudRuntimeWithDiscord(mock.client);
const registry = createSensitiveRequestDispatchRegistry();
registry.register(discordDmSensitiveRequestAdapter);
const request = makeRequest({
kind: "secret",
callback: { url: HOSTED_LINK },
// Secret-adjacent material the adapter must never serialize into the DM.
secretValue: SECRET_SENTINEL,
} as Partial<SensitiveRequest>);
const result = await registry
.resolve?.("dm", "user-snowflake-1234", runtime)
?.deliver({ request, channelId: "user-snowflake-1234", runtime });
expect(result?.delivered).toBe(true);
const sent = mock.dmChannel.send.mock.calls[0]?.[0] as { content: string };
expect(sent.content).toContain(HOSTED_LINK);
expect(sent.content).not.toContain(SECRET_SENTINEL);
});
});