create-eliza
Version:
Initialize an Eliza project
633 lines (624 loc) • 22.8 kB
JavaScript
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import {
loadProject
} from "./chunk-HOYDBWZH.js";
import {
AgentServer,
jsonToCharacter,
loadCharacterTryPath,
promptForEnvVars
} from "./chunk-4XN5G4Z4.js";
import {
require_main
} from "./chunk-ZMJ3QLUC.js";
import {
AgentRuntime,
logger,
settings,
stringToUuid
} from "./chunk-DWYHKJQZ.js";
import {
Command
} from "./chunk-S757QXWN.js";
import {
__toESM
} from "./chunk-WCMDOJQK.js";
// src/commands/test.ts
import * as fs from "node:fs";
import { existsSync as existsSync2, readFileSync } from "node:fs";
import * as net from "node:net";
import * as os from "node:os";
import path from "node:path";
var dotenv = __toESM(require_main(), 1);
// src/testRunner.ts
var TestRunner = class {
runtime;
projectAgent;
stats;
isDirectPluginTest;
constructor(runtime, projectAgent) {
this.runtime = runtime;
this.projectAgent = projectAgent;
this.stats = {
total: 0,
passed: 0,
failed: 0,
skipped: 0
};
if (projectAgent?.plugins?.length === 1 && (projectAgent.character.name.includes(`Test Agent for ${projectAgent.plugins[0].name}`) || projectAgent.character.name.toLowerCase().includes("test") && projectAgent.character.name.toLowerCase().includes(projectAgent.plugins[0].name.toLowerCase())) || // Alternatively, if we were launched from within a plugin directory, consider it a direct test
process.env.ELIZA_TESTING_PLUGIN === "true") {
this.isDirectPluginTest = true;
logger.debug("This is a direct plugin test - will only run tests for this plugin");
} else {
this.isDirectPluginTest = false;
}
}
/**
* Runs a test suite
* @param suite The test suite to run
*/
async runTestSuite(suite) {
logger.info(`
Running test suite: ${suite.name}`);
for (const test2 of suite.tests) {
this.stats.total++;
try {
logger.info(` Running test: ${test2.name}`);
await test2.fn(this.runtime);
this.stats.passed++;
logger.success(` \u2713 ${test2.name}`);
} catch (error) {
this.stats.failed++;
logger.error(` \u2717 ${test2.name}`);
logger.error(` ${error.message}`);
}
}
}
/**
* Runs project agent tests
*/
async runProjectTests(options) {
if (!this.projectAgent?.tests || options.skipProjectTests || this.isDirectPluginTest) {
if (this.isDirectPluginTest) {
logger.info("Skipping project tests when directly testing a plugin");
}
return;
}
logger.info("\nRunning project tests...");
const testSuites = Array.isArray(this.projectAgent.tests) ? this.projectAgent.tests : [this.projectAgent.tests];
for (const suite of testSuites) {
if (!suite) {
continue;
}
if (options.filter && !suite.name.includes(options.filter)) {
this.stats.skipped++;
continue;
}
await this.runTestSuite(suite);
}
}
/**
* Runs plugin tests
*/
async runPluginTests(options) {
if (options.skipPlugins && !this.isDirectPluginTest) {
return;
}
if (this.isDirectPluginTest) {
logger.info("\nRunning plugin tests...");
const plugin = this.projectAgent?.plugins?.[0];
if (!plugin || !plugin.tests) {
logger.warn(`No tests found for this plugin (${plugin?.name || "unknown plugin"})`);
logger.info("To add tests to your plugin, include a 'tests' property with an array of test suites.");
logger.info("Example:");
logger.info(`
export const myPlugin = {
name: "my-plugin",
description: "My awesome plugin",
// ... other plugin properties ...
tests: [
{
name: "Basic Tests",
tests: [
{
name: "should do something",
fn: async (runtime) => {
// Test code here
}
}
]
}
]
};
`);
return;
}
logger.info(`Found test suites for plugin: ${plugin.name}`);
const testSuites = Array.isArray(plugin.tests) ? plugin.tests : [plugin.tests];
for (const suite of testSuites) {
if (!suite) {
continue;
}
if (options.filter && !suite.name.includes(options.filter)) {
logger.info(`Skipping test suite "${suite.name}" because it doesn't match filter "${options.filter}"`);
this.stats.skipped++;
continue;
}
await this.runTestSuite(suite);
}
} else {
logger.info("\nRunning project plugin tests...");
const plugins = this.projectAgent?.plugins || [];
if (plugins.length === 0) {
logger.info("No plugins defined directly in project to test");
return;
}
logger.info(`Found ${plugins.length} plugins defined in project`);
for (const plugin of plugins) {
try {
if (!plugin) {
continue;
}
logger.info(`Testing plugin: ${plugin.name || "unnamed plugin"}`);
const pluginTests = plugin.tests;
if (!pluginTests) {
logger.info(`No tests found for plugin: ${plugin.name || "unnamed plugin"}`);
continue;
}
const testSuites = Array.isArray(pluginTests) ? pluginTests : [pluginTests];
for (const suite of testSuites) {
if (!suite) {
continue;
}
if (options.filter && !suite.name.includes(options.filter)) {
logger.info(`Skipping test suite "${suite.name}" (doesn't match filter)`);
this.stats.skipped++;
continue;
}
await this.runTestSuite(suite);
}
} catch (error) {
logger.error(`Error running tests for plugin ${plugin.name}:`, error);
this.stats.failed++;
}
}
}
}
/**
* Runs all tests in the project
* @param options Test options
*/
async runTests(options = {}) {
await this.runProjectTests(options);
await this.runPluginTests(options);
logger.info(`
Test Summary: ${this.stats.passed} passed, ${this.stats.failed} failed, ${this.stats.skipped} skipped`);
return this.stats;
}
};
// src/commands/test.ts
async function checkPortAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", () => {
resolve(false);
});
server.once("listening", () => {
server.close();
resolve(true);
});
server.listen(port);
});
}
async function startAgent(character, server, init, plugins = []) {
character.id ??= stringToUuid(character.name);
try {
if (server.database) {
try {
logger.debug(`Creating agent directly in database: ${character.name}`);
await server.database.createAgent({
id: character.id,
name: `${character.name} (DB)`,
// Use template literal instead of concatenation
bio: character.bio || "",
system: character.system || "",
enabled: true,
createdAt: Date.now(),
updatedAt: Date.now()
});
logger.debug(`Created agent in database: ${character.id}`);
} catch (error) {
logger.debug(`Note: Agent creation might have failed (possibly already exists): ${error instanceof Error ? error.message : String(error)}`);
}
try {
const agent = await server.database.getAgent(character.id);
if (agent) {
logger.debug(`Verified agent exists in database with ID: ${character.id}`);
} else {
logger.warn(`Warning: Agent does not exist in database after creation attempt: ${character.id}`);
}
} catch (verifyError) {
logger.warn(`Warning: Error verifying agent: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`);
}
}
const validPlugins = plugins.filter((plugin) => plugin != null);
if (validPlugins.length > 0) {
logger.debug(`Starting agent with ${validPlugins.length} plugins:`);
validPlugins.forEach((plugin, i) => {
logger.debug(` Plugin ${i + 1}: ${plugin.name || "unnamed"}`);
logger.debug(` Plugin properties: ${Object.keys(plugin).join(", ")}`);
logger.debug(` Plugin init type: ${typeof plugin.init}`);
});
} else if (plugins.length > 0) {
logger.warn(`Found ${plugins.length} plugins but none were valid objects`);
}
const runtime = new AgentRuntime({
character,
plugins: validPlugins
});
const originalInitialize = runtime.initialize.bind(runtime);
runtime.initialize = async () => {
try {
return await originalInitialize();
} catch (err) {
if (err instanceof Error && (err.message.includes("foreign key constraint") || err.message.includes("does not exist in database"))) {
logger.warn(`Note: Ignoring common initialization error: ${err.message}`);
return;
}
throw err;
}
};
if (init) {
try {
await init(runtime);
} catch (initError) {
logger.error(`Error in agent init function for ${character.name}:`, initError);
throw initError;
}
}
try {
await runtime.initialize();
} catch (initError) {
logger.error(`Error initializing runtime for ${character.name}:`, initError);
if (validPlugins.length > 0) {
logger.debug(`Agent has ${validPlugins.length} plugins:`);
validPlugins.forEach((plugin, index) => {
logger.debug(` Plugin ${index + 1}: ${plugin.name || "unnamed"}`);
if (plugin.init && typeof plugin.init !== "function") {
logger.error(` Plugin ${plugin.name} has an init property that is not a function`);
}
});
}
throw initError;
}
server.registerAgent(runtime);
logger.debug(`Started ${runtime.character.name} as ${runtime.agentId}`);
return runtime;
} catch (error) {
logger.error(`Failed to start agent ${character.name}:`, error);
throw error;
}
}
var runAgentTests = async (options) => {
try {
const runtimes = [];
const projectAgents = [];
const homeDir = os.homedir();
const elizaDir = path.join(homeDir, ".eliza");
const elizaDbDir = path.join(elizaDir, "db");
const envFilePath = path.join(elizaDir, ".env");
logger.info("Setting up environment...");
logger.info(`Home directory: ${homeDir}`);
logger.info(`Eliza directory: ${elizaDir}`);
logger.info(`Database directory: ${elizaDbDir}`);
logger.info(`Environment file: ${envFilePath}`);
if (!fs.existsSync(elizaDir)) {
logger.info(`Creating directory: ${elizaDir}`);
fs.mkdirSync(elizaDir, { recursive: true });
logger.info(`Created directory: ${elizaDir}`);
}
if (!fs.existsSync(elizaDbDir)) {
logger.info(`Creating database directory: ${elizaDbDir}`);
fs.mkdirSync(elizaDbDir, { recursive: true });
logger.info(`Created database directory: ${elizaDbDir}`);
}
process.env.PGLITE_DATA_DIR = elizaDbDir;
logger.info(`Using database directory: ${elizaDbDir}`);
if (fs.existsSync(envFilePath)) {
logger.info(`Loading environment variables from: ${envFilePath}`);
dotenv.config({ path: envFilePath });
logger.info("Environment variables loaded");
} else {
logger.warn(`Environment file not found: ${envFilePath}`);
}
try {
logger.info("Configuring database...");
await promptForEnvVars("pglite");
logger.info("Database configuration completed");
} catch (error) {
logger.error("Error configuring database:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
throw error;
}
const postgresUrl = process.env.POSTGRES_URL;
logger.info(`PostgreSQL URL: ${postgresUrl ? "found" : "not found"}`);
logger.info("Creating server instance...");
const server = new AgentServer({
dataDir: elizaDbDir,
postgresUrl
});
logger.info("Server instance created");
logger.info("Waiting for database initialization...");
try {
await new Promise((resolve, reject) => {
let initializationAttempts = 0;
const maxAttempts = 5;
const checkInterval = setInterval(async () => {
try {
if (server.database?.isInitialized) {
clearInterval(checkInterval);
resolve();
return;
}
initializationAttempts++;
try {
await server.database?.init();
clearInterval(checkInterval);
resolve();
} catch (initError) {
logger.warn(`Database initialization attempt ${initializationAttempts}/${maxAttempts} failed:`, initError);
if (initializationAttempts >= maxAttempts) {
if (server.database?.connection) {
logger.warn("Max initialization attempts reached, but database connection exists. Proceeding anyway.");
clearInterval(checkInterval);
resolve();
} else {
clearInterval(checkInterval);
reject(new Error(`Database initialization failed after ${maxAttempts} attempts`));
}
}
}
} catch (error) {
logger.error("Error during database initialization check:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
clearInterval(checkInterval);
reject(error);
}
}, 1e3);
setTimeout(() => {
clearInterval(checkInterval);
if (server.database?.connection) {
logger.warn("Database initialization timeout, but connection exists. Proceeding anyway.");
resolve();
} else {
reject(new Error("Database initialization timed out after 30 seconds"));
}
}, 3e4);
});
logger.info("Database initialized successfully");
} catch (error) {
logger.error("Failed to initialize database:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
throw error;
}
logger.info("Setting up server properties...");
server.startAgent = async (character) => {
logger.info(`Starting agent for character ${character.name}`);
return startAgent(character, server);
};
server.loadCharacterTryPath = loadCharacterTryPath;
server.jsonToCharacter = jsonToCharacter;
logger.info("Server properties set up");
let serverPort = options.port || Number.parseInt(settings.SERVER_PORT || "3000");
const isLikelyPluginDir = checkIfLikelyPluginDir(process.cwd());
logger.info(`Loading ${isLikelyPluginDir ? "plugin" : "project"} from current directory: ${process.cwd()}`);
if (isLikelyPluginDir) {
process.env.ELIZA_TESTING_PLUGIN = "true";
logger.debug("Set ELIZA_TESTING_PLUGIN=true for direct plugin testing");
}
let project;
try {
logger.debug("Attempting to load module...");
project = await loadProject(process.cwd());
if (project.isPlugin) {
logger.info(`Plugin loaded successfully: ${project.pluginModule?.name}`);
logger.info(`Description: ${project.pluginModule?.description}`);
if (project.pluginModule?.tests) {
const testSuites = Array.isArray(project.pluginModule.tests) ? project.pluginModule.tests : [project.pluginModule.tests];
logger.info(`Plugin has ${testSuites.length} test suites:`);
testSuites.forEach((suite, i) => {
if (suite) {
logger.info(` Suite ${i + 1}: ${suite.name} (${suite.tests?.length || 0} tests)`);
}
});
} else {
logger.info("Plugin does not have any test suites defined");
}
logger.info("Created a test agent to run plugin tests");
} else {
logger.info("Project loaded successfully:", JSON.stringify(project, null, 2));
}
if (!project || !project.agents || project.agents.length === 0) {
throw new Error("No agents found in project configuration");
}
logger.info(`Found ${project.agents.length} agents in ${project.isPlugin ? "plugin" : "project"} configuration`);
} catch (error) {
if (isLikelyPluginDir) {
logger.error("Error loading plugin for testing:", error);
if (error instanceof Error && error.message.includes("No agents found in project")) {
logger.error("This appears to be a plugin directory, but we couldn't load the plugin properly.");
logger.error("Make sure your plugin exports a valid plugin object with name and description properties.");
logger.error("Example plugin structure:");
logger.error(`
export const myPlugin = {
name: "my-plugin",
description: "Description of my plugin",
// other plugin properties
};
export default myPlugin;
`);
}
} else {
logger.error("Error loading project:", error);
}
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
throw error;
}
logger.info("Checking port availability...");
while (!await checkPortAvailable(serverPort)) {
logger.warn(`Port ${serverPort} is in use, trying ${serverPort + 1}`);
serverPort++;
}
logger.info(`Using port ${serverPort}`);
logger.info("Starting server...");
try {
await server.start(serverPort);
logger.info("Server started successfully");
} catch (error) {
logger.error("Error starting server:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
throw error;
}
try {
logger.info(`Found ${project.agents.length} agents in ${project.isPlugin ? "plugin" : "project"}`);
for (const agent of project.agents) {
try {
logger.info(`Starting agent: ${agent.character.name}`);
const runtime = await startAgent(
agent.character,
server,
agent.init,
agent.plugins || []
);
runtimes.push(runtime);
projectAgents.push(agent);
await new Promise((resolve) => setTimeout(resolve, 1e3));
} catch (agentError) {
logger.error(
`Error starting agent ${agent.character.name}:`,
agentError
);
if (agentError instanceof Error) {
logger.error("Error details:", agentError.message);
logger.error("Stack trace:", agentError.stack);
}
throw agentError;
}
}
if (runtimes.length === 0) {
throw new Error("Failed to start any agents from project");
}
logger.info(`Successfully started ${runtimes.length} agents from ${project.isPlugin ? "plugin" : "project"}`);
let totalFailed = 0;
for (let i = 0; i < runtimes.length; i++) {
const runtime = runtimes[i];
const projectAgent = projectAgents[i];
if (project.isPlugin) {
logger.info(`Running tests for plugin: ${project.pluginModule?.name}`);
} else {
logger.info(`Running tests for agent: ${runtime.character.name}`);
}
const testRunner = new TestRunner(runtime, projectAgent);
const skipPlugins = project.isPlugin ? true : options.skipPlugins;
const results = await testRunner.runTests({
filter: options.plugin,
skipPlugins,
skipProjectTests: options.skipProjectTests
});
totalFailed += results.failed;
}
await server.stop();
process.exit(totalFailed > 0 ? 1 : 0);
} catch (error) {
logger.error("Error running tests:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
}
await server.stop();
throw error;
}
} catch (error) {
logger.error("Error in runAgentTests:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
} else {
logger.error("Unknown error type:", typeof error);
logger.error("Error value:", error);
try {
logger.error("Stringified error:", JSON.stringify(error, null, 2));
} catch (e) {
logger.error("Could not stringify error:", e);
}
}
throw error;
}
};
function checkIfLikelyPluginDir(dir) {
const packageJsonPath = path.join(dir, "package.json");
if (existsSync2(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
if (packageJson.name?.includes("plugin")) {
return true;
}
if (packageJson.keywords?.some((k) => k.includes("elizaos") || k.includes("eliza") || k.includes("plugin"))) {
return true;
}
} catch (e) {
}
}
return existsSync2(path.join(dir, "src/plugin.ts")) || existsSync2(path.join(dir, "src/index.ts")) && !existsSync2(path.join(dir, "src/agent.ts")) || dir.includes("plugin");
}
var test = new Command().name("test").description("Run tests for Eliza agent plugins").option(
"-p, --port <port>",
"Port to listen on",
(val) => Number.parseInt(val)
).option("-P, --plugin <name>", "Name of plugin to test").option("--skip-plugins", "Skip plugin tests").option("--skip-project-tests", "Skip project tests").action(async (options) => {
logger.info("Starting test command...");
logger.info("Command options:", options);
try {
logger.info("Running agent tests...");
await runAgentTests(options);
} catch (error) {
logger.error("Error running tests:", error);
if (error instanceof Error) {
logger.error("Error details:", error.message);
logger.error("Stack trace:", error.stack);
} else {
logger.error("Unknown error type:", typeof error);
logger.error("Error value:", error);
try {
logger.error("Stringified error:", JSON.stringify(error, null, 2));
} catch (e) {
logger.error("Could not stringify error:", e);
}
}
process.exit(1);
}
});
function registerCommand(cli) {
return cli.addCommand(test);
}
export {
test,
registerCommand
};
//# sourceMappingURL=chunk-ZRBOK25A.js.map