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
362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
/**
|
||
* Deterministic-handler coverage for the slash-command actions (`/help`,
|
||
* `/status`, `/whoami`, runtime option setters). Exercises `runCommand` /
|
||
* `resolveCommand` against an in-memory runtime stub — no live model.
|
||
*/
|
||
import type { IAgentRuntime, Memory } from "@elizaos/core";
|
||
import { beforeEach, describe, expect, it } from "vitest";
|
||
import {
|
||
commandActions,
|
||
commandShortcuts,
|
||
dispatchCommandMessage,
|
||
explicitCommandShortcuts,
|
||
getCommandSettings,
|
||
naturalShortcuts,
|
||
resolveCommand,
|
||
} from "../src/actions";
|
||
import {
|
||
getEnabledCommandsForRuntime,
|
||
initForRuntime,
|
||
registerCommand,
|
||
useRuntime,
|
||
} from "../src/registry";
|
||
|
||
function makeRuntime(
|
||
agentId = "agent-1",
|
||
onGetCache?: (key: string) => void | Promise<void>,
|
||
overrides: Partial<IAgentRuntime> = {},
|
||
): IAgentRuntime {
|
||
const cache = new Map<string, unknown>();
|
||
let messages: Memory[] = [msg("first"), msg("second")];
|
||
return {
|
||
agentId,
|
||
character: { name: "Eliza", settings: {} },
|
||
actions: [],
|
||
getSetting: () => null,
|
||
setSetting: () => undefined,
|
||
getCache: async (key: string) => {
|
||
await onGetCache?.(key);
|
||
return cache.get(key);
|
||
},
|
||
setCache: async (key: string, value: unknown) => {
|
||
cache.set(key, value);
|
||
return true;
|
||
},
|
||
deleteCache: async (key: string) => cache.delete(key),
|
||
countMemories: async () => messages.length,
|
||
deleteAllMemories: async () => {
|
||
messages = [];
|
||
},
|
||
useModel: async () => "",
|
||
...overrides,
|
||
} as unknown as IAgentRuntime;
|
||
}
|
||
|
||
function msg(text: string, source = "client_chat"): Memory {
|
||
return {
|
||
id: "00000000-0000-0000-0000-000000000001",
|
||
entityId: "00000000-0000-0000-0000-0000000000aa",
|
||
roomId: "room-1",
|
||
content: { text, source },
|
||
} as unknown as Memory;
|
||
}
|
||
|
||
describe("runCommand / resolveCommand — deterministic handlers (#8790)", () => {
|
||
let runtime: IAgentRuntime;
|
||
beforeEach(() => {
|
||
initForRuntime("agent-1");
|
||
runtime = makeRuntime();
|
||
});
|
||
|
||
it("/help lists the registered commands deterministically", async () => {
|
||
const r = await resolveCommand(runtime, msg("/help"));
|
||
expect(r.handled).toBe(true);
|
||
expect(r.reply).toContain("Available commands");
|
||
expect(r.reply).toContain("/help");
|
||
});
|
||
|
||
it("/status reports real runtime state", async () => {
|
||
const r = await resolveCommand(runtime, msg("/status"));
|
||
expect(r.handled).toBe(true);
|
||
expect(r.reply).toContain("Agent: Eliza");
|
||
expect(r.reply).toContain("Commands enabled:");
|
||
});
|
||
|
||
it("/status keeps its runtime-scoped store across awaited settings reads", async () => {
|
||
initForRuntime("agent-a");
|
||
useRuntime("agent-a");
|
||
registerCommand({
|
||
key: "agent-a-only",
|
||
description: "Runtime-local command",
|
||
textAliases: ["/agent-a-only"],
|
||
scope: "both",
|
||
category: "skills",
|
||
});
|
||
initForRuntime("agent-b");
|
||
|
||
const agentACommandCount = getEnabledCommandsForRuntime("agent-a").length;
|
||
expect(agentACommandCount).toBe(
|
||
getEnabledCommandsForRuntime("agent-b").length + 1,
|
||
);
|
||
|
||
const runtimeA = makeRuntime("agent-a", async () => {
|
||
useRuntime("agent-b");
|
||
});
|
||
const r = await resolveCommand(runtimeA, msg("/status"));
|
||
|
||
expect(r.handled).toBe(true);
|
||
expect(r.reply).toContain(`Commands enabled: ${agentACommandCount}`);
|
||
});
|
||
|
||
it("/whoami reflects the sender context", async () => {
|
||
const r = await resolveCommand(runtime, msg("/whoami"), {
|
||
isAuthorized: true,
|
||
isElevated: true,
|
||
senderName: "Shaw",
|
||
});
|
||
expect(r.reply).toContain("You are Shaw");
|
||
expect(r.reply).toContain("Authorized: yes");
|
||
});
|
||
|
||
it("deterministically persists runtime option commands", async () => {
|
||
const cases = [
|
||
["/think high", "Thinking set to high."],
|
||
["/verbose on", "Verbose set to on."],
|
||
["/reasoning stream", "Reasoning set to stream."],
|
||
["/queue steer", "Queue mode set to steer."],
|
||
["/elevated on", "Elevated mode set to on."],
|
||
["/model Anthropic/Claude-Opus", "Model set to Anthropic/Claude-Opus."],
|
||
["/tts off", "TTS set to off."],
|
||
] as const;
|
||
for (const [text, expectedReply] of cases) {
|
||
const r = await resolveCommand(runtime, msg(text), {
|
||
isAuthorized: true,
|
||
isElevated: true,
|
||
});
|
||
expect(r.handled, text).toBe(true);
|
||
expect(r.reply).toBe(expectedReply);
|
||
}
|
||
expect(await getCommandSettings(runtime, "room-1")).toEqual({
|
||
thinking: "high",
|
||
verbose: "on",
|
||
reasoning: "stream",
|
||
queue: "steer",
|
||
elevated: "on",
|
||
model: "Anthropic/Claude-Opus",
|
||
tts: "off",
|
||
});
|
||
});
|
||
|
||
it("rejects invalid option values deterministically", async () => {
|
||
const r = await resolveCommand(runtime, msg("/think enormous"), {
|
||
isAuthorized: true,
|
||
});
|
||
expect(r.handled).toBe(true);
|
||
expect(r.reply).toContain("Invalid thinking value");
|
||
});
|
||
|
||
it("deterministically handles lifecycle commands (reset/new/compact)", async () => {
|
||
await resolveCommand(runtime, msg("/think high"), { isAuthorized: true });
|
||
const reset = await resolveCommand(runtime, msg("/reset"), {
|
||
isAuthorized: true,
|
||
});
|
||
expect(reset.handled).toBe(true);
|
||
expect(reset.reply).toContain("cleared command settings");
|
||
expect(reset.reply).toContain("2 message(s)");
|
||
expect(await getCommandSettings(runtime, "room-1")).toEqual({});
|
||
|
||
await resolveCommand(runtime, msg("/model gpt-5"));
|
||
const next = await resolveCommand(runtime, msg("/new"), {
|
||
isAuthorized: true,
|
||
});
|
||
expect(next.handled).toBe(true);
|
||
expect(next.reply).toBe(
|
||
"Started a new conversation context for this room.",
|
||
);
|
||
expect(await getCommandSettings(runtime, "room-1")).toEqual({});
|
||
|
||
const compactRuntime = makeRuntime("agent-1", undefined, {
|
||
actions: [
|
||
{
|
||
name: "COMPACT_CONVERSATION",
|
||
description: "Compact",
|
||
validate: async () => true,
|
||
handler: async () => ({
|
||
success: true,
|
||
text: "Compacted 4 older message(s); preserved the latest 2.",
|
||
}),
|
||
},
|
||
],
|
||
});
|
||
const compact = await resolveCommand(compactRuntime, msg("/compact"), {
|
||
isAuthorized: true,
|
||
});
|
||
expect(compact.handled).toBe(true);
|
||
expect(compact.reply).toContain("Compacted 4 older message(s)");
|
||
});
|
||
|
||
it("ignores non-command messages", async () => {
|
||
expect((await resolveCommand(runtime, msg("hello world"))).handled).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it("dispatchCommandMessage fires the callback with the reply", async () => {
|
||
const calls: Array<{ text: string; source?: string }> = [];
|
||
const handled = await dispatchCommandMessage(
|
||
runtime,
|
||
msg("/help"),
|
||
(reply) => {
|
||
calls.push(reply);
|
||
return [];
|
||
},
|
||
);
|
||
expect(handled).toBe(true);
|
||
expect(calls).toHaveLength(1);
|
||
expect(calls[0]?.text).toContain("Available commands");
|
||
});
|
||
});
|
||
|
||
describe("command actions — slash-only validate (#8790)", () => {
|
||
let runtime: IAgentRuntime;
|
||
beforeEach(() => {
|
||
initForRuntime("agent-1");
|
||
runtime = makeRuntime();
|
||
});
|
||
|
||
it("registers one action per deterministic command", () => {
|
||
const names = new Set(commandActions.map((a) => a.name));
|
||
expect(names.has("HELP_COMMAND")).toBe(true);
|
||
expect(names.has("STATUS_COMMAND")).toBe(true);
|
||
expect(names.has("THINK_COMMAND")).toBe(true);
|
||
expect(names.has("MODEL_COMMAND")).toBe(true);
|
||
expect(names.has("TTS_COMMAND")).toBe(true);
|
||
expect(names.has("RESET_COMMAND")).toBe(true);
|
||
expect(names.has("NEW_COMMAND")).toBe(true);
|
||
expect(names.has("COMPACT_COMMAND")).toBe(true);
|
||
expect(names.has("STOP_COMMAND")).toBe(false);
|
||
expect(names.has("RESTART_COMMAND")).toBe(false);
|
||
});
|
||
|
||
it("validate() matches only its own slash command, never plain text", async () => {
|
||
const help = commandActions.find((a) => a.name === "HELP_COMMAND");
|
||
expect(help).toBeDefined();
|
||
expect(await help?.validate(runtime, msg("/help"))).toBe(true);
|
||
expect(await help?.validate(runtime, msg("/status"))).toBe(false);
|
||
expect(await help?.validate(runtime, msg("I need help please"))).toBe(
|
||
false,
|
||
);
|
||
});
|
||
|
||
it("handler produces the deterministic reply via the callback", async () => {
|
||
const status = commandActions.find((a) => a.name === "STATUS_COMMAND");
|
||
const replies: string[] = [];
|
||
const result = await status?.handler(
|
||
runtime,
|
||
msg("/status"),
|
||
undefined,
|
||
undefined,
|
||
async (content) => {
|
||
replies.push(String(content.text));
|
||
return [];
|
||
},
|
||
);
|
||
expect(result?.success).toBe(true);
|
||
expect(replies[0]).toContain("Agent: Eliza");
|
||
});
|
||
|
||
it("similes are slash-only (no natural language)", () => {
|
||
for (const action of commandActions) {
|
||
for (const simile of action.similes ?? []) {
|
||
expect(simile.startsWith("/")).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("command shortcuts ↔ actions linkage (#8790 × #8791)", () => {
|
||
it("every shortcut targets a registered command action", () => {
|
||
const actionNames = new Set(commandActions.map((a) => a.name));
|
||
expect(commandShortcuts.length).toBeGreaterThan(0);
|
||
// commandShortcuts = explicit slash shortcuts + narrow natural ones.
|
||
expect(commandShortcuts).toEqual([
|
||
...explicitCommandShortcuts,
|
||
...naturalShortcuts,
|
||
]);
|
||
for (const shortcut of commandShortcuts) {
|
||
expect(shortcut.target.kind).toBe("action");
|
||
if (shortcut.target.kind === "action") {
|
||
expect(actionNames.has(shortcut.target.name)).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("explicit slash shortcuts are explicit-only with slash aliases", () => {
|
||
expect(explicitCommandShortcuts.length).toBeGreaterThan(0);
|
||
for (const shortcut of explicitCommandShortcuts) {
|
||
expect(shortcut.kind).toBe("explicit");
|
||
expect(shortcut.aliases && shortcut.aliases.length > 0).toBe(true);
|
||
for (const alias of shortcut.aliases ?? []) {
|
||
expect(alias.startsWith("/")).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("natural shortcuts carry no slash aliases (anchored patterns)", () => {
|
||
for (const shortcut of naturalShortcuts) {
|
||
expect(shortcut.kind).toBe("natural");
|
||
expect(shortcut.aliases ?? []).toHaveLength(0);
|
||
expect(shortcut.patterns && shortcut.patterns.length > 0).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("records every shortcut alias/action signature for the e2e coverage matrix", () => {
|
||
const signatures = commandShortcuts
|
||
.flatMap((shortcut) => {
|
||
const target =
|
||
shortcut.target.kind === "action"
|
||
? shortcut.target.name
|
||
: shortcut.target.kind;
|
||
return (shortcut.aliases ?? []).map(
|
||
(alias) => `${shortcut.id}:${alias}->${target}`,
|
||
);
|
||
})
|
||
.sort();
|
||
|
||
expect(signatures).toEqual([
|
||
"cmd:accounts:/accounts->ACCOUNTS_COMMAND",
|
||
"cmd:backend:/backend->BACKEND_COMMAND",
|
||
"cmd:commands:/cmds->COMMANDS_COMMAND",
|
||
"cmd:commands:/commands->COMMANDS_COMMAND",
|
||
"cmd:compact:/compact->COMPACT_COMMAND",
|
||
"cmd:context:/context->CONTEXT_COMMAND",
|
||
"cmd:context:/ctx->CONTEXT_COMMAND",
|
||
"cmd:elevated:/elev->ELEVATED_COMMAND",
|
||
"cmd:elevated:/elevated->ELEVATED_COMMAND",
|
||
"cmd:help:/?->HELP_COMMAND",
|
||
"cmd:help:/h->HELP_COMMAND",
|
||
"cmd:help:/help->HELP_COMMAND",
|
||
"cmd:model:/m->MODEL_COMMAND",
|
||
"cmd:model:/model->MODEL_COMMAND",
|
||
"cmd:new:/new->NEW_COMMAND",
|
||
"cmd:queue:/q->QUEUE_COMMAND",
|
||
"cmd:queue:/queue->QUEUE_COMMAND",
|
||
"cmd:reasoning:/reason->REASONING_COMMAND",
|
||
"cmd:reasoning:/reasoning->REASONING_COMMAND",
|
||
"cmd:reset:/reset->RESET_COMMAND",
|
||
"cmd:status:/s->STATUS_COMMAND",
|
||
"cmd:status:/status->STATUS_COMMAND",
|
||
"cmd:think:/t->THINK_COMMAND",
|
||
"cmd:think:/think->THINK_COMMAND",
|
||
"cmd:think:/thinking->THINK_COMMAND",
|
||
"cmd:tts:/tts->TTS_COMMAND",
|
||
"cmd:tts:/voice->TTS_COMMAND",
|
||
"cmd:usage:/usage->USAGE_COMMAND",
|
||
"cmd:verbose:/v->VERBOSE_COMMAND",
|
||
"cmd:verbose:/verbose->VERBOSE_COMMAND",
|
||
"cmd:whoami:/who->WHOAMI_COMMAND",
|
||
"cmd:whoami:/whoami->WHOAMI_COMMAND",
|
||
]);
|
||
});
|
||
});
|