Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/examples/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,21 @@ The local examples sweep has been run in this worktree with these outcomes:
| --- | --- | --- |
| `_plugin` | `typecheck`, `test`, `build` | Optional manual Cypress flow via `test:e2e:manual`. |
| `a2a` | `typecheck`, `test`, `build` | `OPENAI_API_KEY` for model-backed mode. |
| `agent-console` | `typecheck` | Browser session plus one provider key to inspect live SSE telemetry. |
| `agent-console` | `test`, `typecheck` | Action scanner fixture test; browser session plus one provider key to inspect live SSE telemetry. |
| `app/capacitor` | Parent skip scripts plus backend/frontend package checks | Native Capacitor device/simulator testing and provider keys. |
| `app/capacitor/backend` | `typecheck`, `test`, `build` | Provider key and device/simulator flow through the Capacitor shell. |
| `app/capacitor/frontend` | `typecheck`, `build` | Browser and native WebView smoke test against a configured backend. |
| `app/electron` | Parent skip scripts plus backend/frontend package checks | Desktop Electron launch and provider-key chat flow. |
| `app/electron/backend` | `typecheck`, `test`, `build` | Provider-key chat flow from the packaged Electron shell. |
| `app/electron/frontend` | `typecheck`, `build` | Renderer smoke test in Electron and browser dev-server mode. |
| `autonomous` | `typecheck`, `build` | Optional local model and shell sandbox configuration. |
| `autonomous` | `test`, `typecheck`, `build` | Decision parser, shell allowlist, and prompt tests; optional local model and shell sandbox configuration. |
| `avatar` | `typecheck`, `build` | Browser microphone/audio flow, selected model key, optional ElevenLabs key. |
| `aws` | `typecheck`, `test`, `build` | AWS account, SAM deployment, and Lambda invocation with `OPENAI_API_KEY`. |
| `bluesky` | `typecheck`, `test`, `build` | `LIVE_TEST=true` with Bluesky credentials and dry-run/posting flags. |
| `browser-extension` | Parent typecheck skip and documented Chrome/Safari package checks | Load unpacked Chrome extension; Safari requires Xcode signing/install. |
| `browser-extension/chrome` | `typecheck`, explicit build skip | `build:tsup` only after resolving browser bundling of Node-only workspace deps; load unpacked for runtime validation. |
| `browser-extension/safari` | Typecheck skip, scripted Safari build path | Xcode and Safari extension signing. |
| `chat` | `typecheck`, `build` | One configured provider key for live chat. |
| `chat` | `test`, `typecheck`, `build` | Provider-selection tests; one configured provider key for live chat. |
| `cloud/clone-ur-crush` | `typecheck`, `test`, `build` | Live Next.js flow with required model/image provider keys. |
| `cloud/edad` | `typecheck`, `test`, `build` | Manual server launch with Eliza Cloud app ID, affiliate code, and signed-in user token. |
| `cloudflare` | `typecheck`, `test`, `build` | Wrangler login, Worker secret, deployed or local Worker endpoint. |
Expand All @@ -86,11 +86,11 @@ The local examples sweep has been run in this worktree with these outcomes:
| `roblox` | `typecheck`, `test`, `build` | Roblox Studio place, Open Cloud key, tunnel/shared-secret bridge test. |
| `smartglasses` | `typecheck`, `test` | Even Realities simulator or BLE hardware evidence report. |
| `supabase` | Static review; no package scripts | Supabase CLI/Deno function serve or deployment with anon key and `OPENAI_API_KEY`. |
| `telegram` | `typecheck`, `build` | Telegram bot token and provider key. |
| `text-adventure` | `typecheck`, `build` | Optional manual CLI playthrough. |
| `telegram` | `test`, `typecheck`, `build` | Env and character wiring tests; Telegram bot token and provider key for live run. |
| `text-adventure` | `test`, `typecheck`, `build` | Deterministic no-LLM dungeon-engine playthrough; optional manual CLI playthrough requires `OPENAI_API_KEY`. |
| `tic-tac-toe` | `typecheck`, `test`, `build` | Test runs the non-interactive bench mode. |
| `trader` | `typecheck`, `build` | Paper-trading UI flow, then isolated-wallet live testing only when intended. |
| `twitter-xai` | `typecheck`, `build` | X/xAI credentials; start with `TWITTER_DRY_RUN=true`. |
| `twitter-xai` | `test`, `typecheck`, `build` | Credential-mode validation tests; X/xAI credentials for live run, starting with `TWITTER_DRY_RUN=true`. |
| `vercel` | `typecheck`, `test`, `build` | Vercel project/env plus deployed or `vercel dev` API endpoint. |

## Not Yet Proven By Local Automation
Expand Down
6 changes: 4 additions & 2 deletions packages/examples/agent-console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ providers used here do not expose an embeddings endpoint.
## Validate

```bash
bun run test
bun run typecheck
```

The example has no external service test because it requires a live model
provider key and an interactive browser session.
The local test covers the action scanner against a fixture repository. A full
dashboard session still requires a live model provider key and an interactive
browser.
72 changes: 72 additions & 0 deletions packages/examples/agent-console/action-scanner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { scanRepoActions } from "./action-scanner";

test("scanRepoActions discovers action metadata and subaction links", () => {
const repoRoot = mkdtempSync(join(tmpdir(), "agent-console-scan-"));
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
const fixtureDir = join(repoRoot, "packages", "fixture");
mkdirSync(fixtureDir, { recursive: true });
writeFileSync(
join(fixtureDir, "actions.ts"),
`
const childAction = {
name: "lookup_weather",
description: "Look up weather for a city",
parameters: [
{
name: "city",
description: "City to query",
required: true,
schema: { type: "string" },
},
],
validate: async () => true,
handler: async () => undefined,
};

export const parentAction = {
name: "travel_plan",
description: "Plan a trip",
parameters: [
{
name: "mode",
description: "Operation to perform",
schema: { type: "string", enum: ["lookup_weather"] },
},
],
subActions: ["lookup_weather"],
validate: async (runtime) => Boolean(runtime),
handler: async () => undefined,
};
`,
);

const result = scanRepoActions({ repoRoot });
const parent = result.actions.find((action) => action.name === "travel_plan");
const child = result.actions.find(
(action) => action.name === "lookup_weather",
);

expect(result.filesScanned).toBe(1);
expect(result.actionCount).toBe(2);
expect(parent?.validation).toBe("conditional");
expect(parent?.resolvedSubActions).toEqual([
expect.objectContaining({
found: true,
name: "lookup_weather",
targetId: child?.id,
}),
]);
expect(parent?.inferredSubActions).toEqual([
{
name: "lookup_weather",
parameter: "mode",
source: "parameter-enum",
},
]);
expect(child?.parameterSummary).toEqual(["city:string required"]);
});
1 change: 1 addition & 0 deletions packages/examples/agent-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"start": "bun run server.ts",
"dev": "bun --hot run server.ts",
"test": "bun test action-scanner.test.ts",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/examples/autonomous/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ cd packages/examples/autonomous
bun install
bun run start
```

## Validate

```bash
bun run test
bun run typecheck
```

The test suite covers the local decision parser, shell-command allowlist, and
prompt construction without starting local inference or shell services.
56 changes: 56 additions & 0 deletions packages/examples/autonomous/autonomous.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from "bun:test";
import { decisionPrompt, isCommandAllowed, parseDecision } from "./autonomous";

test("parseDecision accepts valid JSON decisions and clamps sleep", () => {
expect(
parseDecision('{"action":"RUN","command":"ls","note":"inspect"}'),
).toEqual({
action: "RUN",
command: "ls",
note: "inspect",
});
expect(parseDecision('{"action":"SLEEP","sleepMs":1,"note":"wait"}')).toEqual(
{
action: "SLEEP",
sleepMs: 100,
note: "wait",
},
);
expect(parseDecision('{"action":"STOP","note":"done"}')).toEqual({
action: "STOP",
note: "done",
});
});

test("parseDecision rejects incomplete or malformed decisions", () => {
expect(parseDecision("not json")).toBeNull();
expect(parseDecision('{"action":"RUN","note":"missing command"}')).toBeNull();
expect(parseDecision('{"action":"SLEEP","sleepMs":"nope"}')).toBeNull();
expect(parseDecision('{"action":"DELETE","note":"bad action"}')).toBeNull();
});

test("isCommandAllowed blocks shell metacharacters and unknown commands", () => {
const allowed = ["ls", "pwd", "cat", "echo"];

expect(isCommandAllowed("ls -la", allowed)).toBe(true);
expect(isCommandAllowed(" echo hello ", allowed)).toBe(true);
expect(isCommandAllowed("rm file", allowed)).toBe(false);
expect(isCommandAllowed("ls && rm file", allowed)).toBe(false);
expect(isCommandAllowed("cat file | grep x", allowed)).toBe(false);
expect(isCommandAllowed("echo hello\npwd", allowed)).toBe(false);
});

test("decisionPrompt includes sandbox, commands, goal, and history", () => {
const prompt = decisionPrompt({
goal: "Write STATUS.txt",
allowedDirectory: "/tmp/sandbox",
allowedCommands: ["ls", "echo"],
recentSteps: "[step 1] SLEEP",
});

expect(prompt).toContain("Write STATUS.txt");
expect(prompt).toContain("/tmp/sandbox");
expect(prompt).toContain("ls, echo");
expect(prompt).toContain("[step 1] SLEEP");
expect(prompt).toContain('"action": "RUN|SLEEP|STOP"');
});
26 changes: 14 additions & 12 deletions packages/examples/autonomous/autonomous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ import {
} from "@elizaos/core";
import { v4 as uuidv4 } from "uuid";

const { default: inmemorydbPlugin } = await import(
"@elizaos/plugin-inmemorydb"
);
const { default: localInferencePlugin } = await import(
"@elizaos/plugin-local-inference"
);
const { shellPlugin } = await import("@elizaos/plugin-shell");

type ShellService = {
executeCommand(
command: string,
Expand Down Expand Up @@ -109,7 +101,7 @@ function readStringField(
return null;
}

function parseDecision(raw: string): AgentDecision | null {
export function parseDecision(raw: string): AgentDecision | null {
const parsed = parseJSONObjectFromText(raw);
if (!parsed) return null;

Expand Down Expand Up @@ -147,7 +139,7 @@ function baseCommand(command: string): string {
return space === -1 ? trimmed : trimmed.slice(0, space);
}

function isCommandAllowed(
export function isCommandAllowed(
command: string,
allowedBaseCommands: readonly string[],
): boolean {
Expand All @@ -163,7 +155,7 @@ function isCommandAllowed(
return allowedBaseCommands.includes(cmd);
}

function decisionPrompt(params: {
export function decisionPrompt(params: {
goal: string;
allowedDirectory: string;
allowedCommands: readonly string[];
Expand Down Expand Up @@ -202,6 +194,14 @@ Rules:
}

async function main(): Promise<void> {
const { default: inmemorydbPlugin } = await import(
"@elizaos/plugin-inmemorydb"
);
const { default: localInferencePlugin } = await import(
"@elizaos/plugin-local-inference"
);
const { shellPlugin } = await import("@elizaos/plugin-shell");

const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "..", "..", "..");

Expand Down Expand Up @@ -477,4 +477,6 @@ async function main(): Promise<void> {
await runtime.stop();
}

await main();
if (import.meta.main) {
await main();
}
1 change: 1 addition & 0 deletions packages/examples/autonomous/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "bun run autonomous.ts",
"dev": "bun --watch run autonomous.ts",
"test": "bun test",
"lint": "bunx @biomejs/biome check --write --unsafe .",
"typecheck": "tsc --noEmit",
"build": "bun run typecheck"
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/autonomous/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"],
"types": ["node", "bun-types"],
"paths": {
"@elizaos/plugin-capacitor-bridge": [
"../../../plugins/plugin-capacitor-bridge/src/index.ts"
Expand Down
5 changes: 5 additions & 0 deletions packages/examples/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ Run with hot reload:
bun run dev
```

Run the local provider-selection checks:
```bash
bun run test
```

## License

MIT
44 changes: 44 additions & 0 deletions packages/examples/chat/chat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { afterEach, expect, test } from "bun:test";
import { detectLLMPlugin, hasValidApiKey, LLM_PROVIDERS } from "./chat";

const providerEnvKeys = LLM_PROVIDERS.map((provider) => provider.envKey);
const originalEnv = Object.fromEntries(
providerEnvKeys.map((envKey) => [envKey, process.env[envKey]]),
);

afterEach(() => {
for (const envKey of providerEnvKeys) {
const originalValue = originalEnv[envKey];
if (originalValue === undefined) {
delete process.env[envKey];
} else {
process.env[envKey] = originalValue;
}
}
});

test("provider list keeps the documented priority order", () => {
expect(LLM_PROVIDERS.map((provider) => provider.name)).toEqual([
"OpenAI",
"Anthropic (Claude)",
"xAI (Grok)",
"Google GenAI (Gemini)",
"Groq",
]);
});

test("empty provider keys are treated as missing", () => {
process.env.OPENAI_API_KEY = " ";
expect(hasValidApiKey("OPENAI_API_KEY")).toBe(false);

process.env.OPENAI_API_KEY = "test-key";
expect(hasValidApiKey("OPENAI_API_KEY")).toBe(true);
});

test("detectLLMPlugin returns null before importing providers when no key is set", async () => {
for (const envKey of providerEnvKeys) {
delete process.env[envKey];
}

await expect(detectLLMPlugin()).resolves.toBeNull();
});
20 changes: 11 additions & 9 deletions packages/examples/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
type UUID,
} from "@elizaos/core";

type LLMProvider = {
export type LLMProvider = {
name: string;
envKey: string;
specifier: string;
};

const LLM_PROVIDERS: LLMProvider[] = [
export const LLM_PROVIDERS: LLMProvider[] = [
{
name: "OpenAI",
envKey: "OPENAI_API_KEY",
Expand Down Expand Up @@ -45,12 +45,12 @@ const LLM_PROVIDERS: LLMProvider[] = [
},
];

function hasValidApiKey(envKey: string): boolean {
export function hasValidApiKey(envKey: string): boolean {
const value = process.env[envKey];
return typeof value === "string" && value.trim().length > 0;
}

async function detectLLMPlugin(): Promise<{
export async function detectLLMPlugin(): Promise<{
plugin: Plugin;
providerName: string;
} | null> {
Expand All @@ -68,7 +68,7 @@ async function detectLLMPlugin(): Promise<{
return null;
}

function printAvailableProviders(): void {
export function printAvailableProviders(): void {
console.log("\nSupported LLM providers and their API keys:\n");
for (const provider of LLM_PROVIDERS) {
const hasKey = hasValidApiKey(provider.envKey);
Expand Down Expand Up @@ -175,7 +175,9 @@ async function main() {
prompt();
}

main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
if (import.meta.main) {
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
}
Loading
Loading