permamind
Version:
An MCP server that provides an immortal memory layer for AI agents and clients
675 lines (662 loc) • 27.1 kB
JavaScript
const service = (aoMessageService, processService) => {
return {
createDefaultTestSuite: async (processDefinition) => {
try {
// Extract handlers from the process definition
const handlers = extractHandlers(processDefinition.compiledLua);
// Create test cases for each handler
const testCases = handlers.map((handler, index) => ({
assertions: [
{
expected: true,
id: `assert-${handler.name}-${index}`,
message: `${handler.name} should return output`,
target: "Output",
type: "exists",
},
],
description: `Test the ${handler.name} handler functionality`,
id: `test-${handler.name}-${index}`,
messages: [
{
action: handler.name,
data: generateTestData(handler.name),
id: `msg-${handler.name}-${index}`,
tags: [{ name: "Action", value: handler.name }],
},
],
name: `Test ${handler.name} Handler`,
timeout: 10000,
}));
return {
configuration: {
concurrent: false,
coverage: true,
timeout: 30000,
verbose: true,
},
id: `test-suite-${processDefinition.id}`,
name: `${processDefinition.name} Test Suite`,
processId: processDefinition.id,
setup: {
initializeProcess: true,
processSource: processDefinition.compiledLua,
},
teardown: {
cleanupProcess: true,
resetState: true,
},
testCases,
};
}
catch (error) {
throw new Error(`Failed to create default test suite: ${error instanceof Error ? error.message : "Unknown error"}`);
}
},
createTestEnvironment: async (processDefinition, configuration = {}) => {
try {
// Create a local simulation environment
const environment = {
configuration: {
concurrent: configuration.concurrent || false,
coverage: configuration.coverage || false,
maxConcurrency: configuration.maxConcurrency || 1,
retries: configuration.retries || 3,
timeout: configuration.timeout || 30000,
verbose: configuration.verbose || false,
},
isRunning: false,
messageQueue: [],
processId: processDefinition.id,
processSource: processDefinition.compiledLua,
state: initializeProcessState(processDefinition),
};
// Initialize the process environment
await initializeEnvironment(environment);
return environment;
}
catch (error) {
throw new Error(`Failed to create test environment: ${error instanceof Error ? error.message : "Unknown error"}`);
}
},
executeTestCase: async (testCase, environment) => {
const startTime = Date.now();
const messageResults = [];
const assertionResults = [];
try {
// Ensure minimum duration for testing
await new Promise((resolve) => setTimeout(resolve, 1));
// Execute test messages
for (const message of testCase.messages) {
const messageResult = await service(aoMessageService, processService).simulateMessage(message, environment);
messageResults.push(messageResult);
// Stop if message failed and we're not continuing on error
if (messageResult.status === "failed" &&
!environment.configuration.concurrent) {
break;
}
}
// Execute assertions
for (const assertion of testCase.assertions) {
const lastMessageResult = messageResults[messageResults.length - 1];
const actualResult = lastMessageResult?.response || environment.state;
const assertionResult = await service(aoMessageService, processService).validateAssertion(assertion, actualResult);
assertionResults.push(assertionResult);
}
// Determine test case status
const hasFailedMessages = messageResults.some((m) => m.status === "failed");
const hasFailedAssertions = assertionResults.some((a) => a.status === "failed");
const status = hasFailedMessages || hasFailedAssertions ? "failed" : "passed";
return {
assertionResults,
duration: Date.now() - startTime,
messageResults,
status,
testCaseId: testCase.id,
};
}
catch (error) {
return {
assertionResults,
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : "Unknown error",
messageResults,
status: "error",
testCaseId: testCase.id,
};
}
},
executeTestSuite: async (testSuite, environment) => {
const startTime = Date.now();
const results = [];
try {
// Ensure minimum duration for testing
await new Promise((resolve) => setTimeout(resolve, 1));
// Execute setup
if (testSuite.setup) {
await executeTestSetup(testSuite.setup, environment);
}
// Execute test cases
if (testSuite.configuration.concurrent) {
// Run tests concurrently
const promises = testSuite.testCases.map((testCase) => service(aoMessageService, processService).executeTestCase(testCase, environment));
const concurrentResults = await Promise.allSettled(promises);
for (const result of concurrentResults) {
if (result.status === "fulfilled") {
results.push(result.value);
}
else {
results.push({
assertionResults: [],
duration: 0,
error: result.reason,
messageResults: [],
status: "error",
testCaseId: "unknown",
});
}
}
}
else {
// Run tests sequentially
for (const testCase of testSuite.testCases) {
const result = await service(aoMessageService, processService).executeTestCase(testCase, environment);
results.push(result);
}
}
// Execute teardown
if (testSuite.teardown) {
await executeTestTeardown(testSuite.teardown, environment);
}
// Calculate results
const totalTests = results.length;
const passedTests = results.filter((r) => r.status === "passed").length;
const failedTests = results.filter((r) => r.status === "failed").length;
const erroredTests = results.filter((r) => r.status === "error").length;
const duration = Date.now() - startTime;
return {
coverage: testSuite.configuration.coverage
? await calculateCoverage(testSuite, environment)
: undefined,
duration,
erroredTests,
failedTests,
passedTests,
results,
status: failedTests > 0 || erroredTests > 0 ? "failed" : "passed",
testSuiteId: testSuite.id,
totalTests,
};
}
catch (error) {
return {
duration: Date.now() - startTime,
erroredTests: testSuite.testCases.length,
failedTests: 0,
passedTests: 0,
results: [],
status: "error",
testSuiteId: testSuite.id,
totalTests: testSuite.testCases.length,
};
}
},
generateTestReport: async (testResults, format = "json") => {
try {
switch (format) {
case "html":
return generateHtmlReport(testResults);
case "json":
return JSON.stringify(testResults, null, 2);
case "markdown":
return generateMarkdownReport(testResults);
default:
throw new Error(`Unsupported report format: ${format}`);
}
}
catch (error) {
throw new Error(`Failed to generate test report: ${error instanceof Error ? error.message : "Unknown error"}`);
}
},
simulateMessage: async (message, environment) => {
const startTime = Date.now();
try {
// Add delay if specified
if (message.delay) {
await new Promise((resolve) => setTimeout(resolve, message.delay));
}
// Ensure minimum duration for testing
await new Promise((resolve) => setTimeout(resolve, 1));
// Create AO message structure
const aoMessage = {
Data: message.data ? JSON.stringify(message.data) : "",
From: "test-sender",
Id: message.id,
Tags: message.tags || [],
Target: environment.processId,
Timestamp: Date.now(),
};
// Execute the message in the simulated environment
const response = await executeInEnvironment(aoMessage, environment);
return {
duration: Date.now() - startTime,
messageId: message.id,
response,
status: "sent",
};
}
catch (error) {
return {
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : "Unknown error",
messageId: message.id,
status: "failed",
};
}
},
validateAssertion: async (assertion, actualResult) => {
try {
let actual;
// Extract the target value using JSONPath or property access
if (assertion.target.includes(".")) {
actual = getNestedProperty(actualResult, assertion.target);
}
else {
actual = actualResult?.[assertion.target] || actualResult;
}
// Perform assertion based on type
let passed = false;
switch (assertion.type) {
case "contains":
passed =
typeof actual === "string" &&
typeof assertion.expected === "string" &&
actual.includes(assertion.expected);
break;
case "custom":
if (assertion.customValidator) {
// Execute custom validator function
const validator = new Function("actual", "expected", assertion.customValidator);
passed = validator(actual, assertion.expected);
}
break;
case "equals":
passed = actual === assertion.expected;
break;
case "exists":
passed = actual !== undefined && actual !== null;
break;
case "matches":
passed =
typeof assertion.expected === "string" &&
new RegExp(assertion.expected).test(String(actual));
break;
}
return {
actual,
assertionId: assertion.id,
expected: assertion.expected,
message: assertion.message,
status: passed ? "passed" : "failed",
};
}
catch (error) {
return {
actual: "Error: " +
(error instanceof Error ? error.message : "Unknown error"),
assertionId: assertion.id,
expected: assertion.expected,
message: assertion.message,
status: "failed",
};
}
},
validateProcessBehavior: async (processId, messages, environment) => {
const startTime = Date.now();
const messageResults = [];
try {
// Execute each message and validate behavior
for (const message of messages) {
const messageResult = await service(aoMessageService, processService).simulateMessage(message, environment);
messageResults.push(messageResult);
// Validate expected response if provided
if (message.expectedResponse && messageResult.response) {
const isValid = validateResponse(messageResult.response, message.expectedResponse);
if (!isValid) {
messageResult.status = "failed";
messageResult.error = "Response does not match expected result";
}
}
}
// Calculate validation results
const totalTests = messages.length;
const passedTests = messageResults.filter((r) => r.status === "sent").length;
const failedTests = messageResults.filter((r) => r.status === "failed").length;
const erroredTests = messageResults.filter((r) => r.status === "timeout").length;
return {
duration: Date.now() - startTime,
erroredTests,
failedTests,
passedTests,
results: messageResults.map((mr) => ({
assertionResults: [],
duration: mr.duration,
error: mr.error,
messageResults: [mr],
status: mr.status === "sent" ? "passed" : "failed",
testCaseId: mr.messageId,
})),
status: failedTests > 0 || erroredTests > 0 ? "failed" : "passed",
testSuiteId: "behavior-validation",
totalTests,
};
}
catch (error) {
return {
duration: Date.now() - startTime,
erroredTests: messages.length,
failedTests: 0,
passedTests: 0,
results: [],
status: "error",
testSuiteId: "behavior-validation",
totalTests: messages.length,
};
}
},
};
};
// Helper functions
const initializeProcessState = (processDefinition) => {
// Initialize default state based on process type
return {
initialized: true,
name: processDefinition.name,
processId: processDefinition.id,
timestamp: Date.now(),
version: processDefinition.version,
};
};
const initializeEnvironment = async (environment) => {
// Initialize the process in the environment
environment.isRunning = true;
environment.state = {
...environment.state,
handlers: extractHandlers(environment.processSource),
};
};
const executeTestSetup = async (setup, environment) => {
if (setup.initializeProcess) {
await initializeEnvironment(environment);
}
if (setup.initialState) {
environment.state = { ...environment.state, ...setup.initialState };
}
if (setup.environment) {
environment.configuration = {
...environment.configuration,
...setup.environment,
};
}
};
const executeTestTeardown = async (teardown, environment) => {
if (teardown.cleanupProcess) {
environment.isRunning = false;
}
if (teardown.resetState) {
environment.state = initializeProcessState({
id: environment.processId,
name: "Reset",
version: "1.0.0",
});
}
if (teardown.cleanupData) {
environment.messageQueue = [];
}
};
const calculateCoverage = async (testSuite, environment) => {
const handlers = extractHandlers(environment.processSource);
const handlerNames = handlers.map((h) => h.name);
// Get covered handlers from test cases
const coveredHandlers = new Set();
for (const testCase of testSuite.testCases) {
for (const message of testCase.messages) {
if (handlerNames.includes(message.action)) {
coveredHandlers.add(message.action);
}
}
}
const coveredHandlerArray = Array.from(coveredHandlers);
const uncoveredHandlers = handlerNames.filter((h) => !coveredHandlerArray.includes(h));
return {
coveragePercentage: (coveredHandlerArray.length / handlerNames.length) * 100,
coveredHandlers: coveredHandlerArray,
handlers: handlerNames,
uncoveredHandlers,
};
};
const extractHandlers = (lua) => {
const handlers = [];
// Extract handler definitions using regex
const handlerMatches = lua.match(/Handlers\.add\([^)]+\)/g);
if (handlerMatches) {
for (const match of handlerMatches) {
const parts = match.match(/Handlers\.add\("([^"]+)",\s*([^,]+),\s*([^)]+)\)/);
if (parts) {
handlers.push({
handler: parts[3],
name: parts[1],
pattern: parts[2],
});
}
}
}
return handlers;
};
const generateTestData = (handlerName) => {
switch (handlerName.toLowerCase()) {
case "balance":
return { target: "test-account" };
case "info":
return {};
case "transfer":
return { quantity: "100", recipient: "test-recipient" };
default:
return { test: true };
}
};
const validateResponse = (actual, expected) => {
// If the actual response has an Output field, parse it and compare
if (actual && actual.Output) {
try {
const parsedOutput = JSON.parse(actual.Output);
if (typeof expected === "object" && expected !== null) {
for (const key in expected) {
if (parsedOutput[key] !== expected[key]) {
return false;
}
}
return true;
}
return parsedOutput === expected;
}
catch {
// If parsing fails, fall back to direct comparison
}
}
// Direct comparison for non-AO response objects
if (typeof expected === "object" && expected !== null) {
for (const key in expected) {
if (actual[key] !== expected[key]) {
return false;
}
}
return true;
}
return actual === expected;
};
const generateMarkdownReport = (testResults) => {
return `# Test Results
## Summary
- **Status**: ${testResults.status}
- **Total Tests**: ${testResults.totalTests}
- **Passed**: ${testResults.passedTests}
- **Failed**: ${testResults.failedTests}
- **Errors**: ${testResults.erroredTests}
- **Duration**: ${testResults.duration}ms
## Test Cases
${testResults.results
.map((result) => `
### ${result.testCaseId}
- **Status**: ${result.status}
- **Duration**: ${result.duration}ms
${result.error ? `- **Error**: ${result.error}` : ""}
#### Assertions
${result.assertionResults
.map((assertion) => `
- **${assertion.assertionId}**: ${assertion.status}
- Expected: ${JSON.stringify(assertion.expected)}
- Actual: ${JSON.stringify(assertion.actual)}
`)
.join("")}
#### Messages
${result.messageResults
.map((message) => `
- **${message.messageId}**: ${message.status} (${message.duration}ms)
${message.error ? ` - Error: ${message.error}` : ""}
`)
.join("")}
`)
.join("")}
${testResults.coverage
? `
## Coverage
- **Handlers**: ${testResults.coverage.handlers.join(", ")}
- **Covered**: ${testResults.coverage.coveredHandlers.join(", ")}
- **Uncovered**: ${testResults.coverage.uncoveredHandlers.join(", ")}
- **Coverage**: ${testResults.coverage.coveragePercentage.toFixed(2)}%
`
: ""}
`;
};
const generateHtmlReport = (testResults) => {
return `<!DOCTYPE html>
<html>
<head>
<title>AOLite Test Results</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
.test-case { border: 1px solid #ddd; margin: 10px 0; padding: 10px; border-radius: 5px; }
.passed { background: #d4edda; }
.failed { background: #f8d7da; }
.error { background: #fff3cd; }
</style>
</head>
<body>
<h1>AOLite Test Results</h1>
<div class="summary">
<h2>Summary</h2>
<p><strong>Status:</strong> ${testResults.status}</p>
<p><strong>Total Tests:</strong> ${testResults.totalTests}</p>
<p><strong>Passed:</strong> ${testResults.passedTests}</p>
<p><strong>Failed:</strong> ${testResults.failedTests}</p>
<p><strong>Errors:</strong> ${testResults.erroredTests}</p>
<p><strong>Duration:</strong> ${testResults.duration}ms</p>
</div>
<h2>Test Cases</h2>
${testResults.results
.map((result) => `
<div class="test-case ${result.status}">
<h3>${result.testCaseId}</h3>
<p><strong>Status:</strong> ${result.status}</p>
<p><strong>Duration:</strong> ${result.duration}ms</p>
${result.error ? `<p><strong>Error:</strong> ${result.error}</p>` : ""}
<h4>Assertions</h4>
<ul>
${result.assertionResults
.map((assertion) => `
<li><strong>${assertion.assertionId}:</strong> ${assertion.status}</li>
`)
.join("")}
</ul>
<h4>Messages</h4>
<ul>
${result.messageResults
.map((message) => `
<li><strong>${message.messageId}:</strong> ${message.status} (${message.duration}ms)</li>
`)
.join("")}
</ul>
</div>
`)
.join("")}
${testResults.coverage
? `
<h2>Coverage</h2>
<p><strong>Coverage:</strong> ${testResults.coverage.coveragePercentage.toFixed(2)}%</p>
<p><strong>Covered Handlers:</strong> ${testResults.coverage.coveredHandlers.join(", ")}</p>
<p><strong>Uncovered Handlers:</strong> ${testResults.coverage.uncoveredHandlers.join(", ")}</p>
`
: ""}
</body>
</html>`;
};
const executeInEnvironment = async (message, environment) => {
// This simulates executing a message in the AOLite environment
// In a real implementation, this would run the Lua code
// Extract action from message tags
const actionTag = message.Tags?.find((tag) => tag.name === "Action");
const action = actionTag?.value || "unknown";
// Simulate different responses based on action
switch (action.toLowerCase()) {
case "balance":
return {
Assignments: {},
Messages: [],
Output: JSON.stringify({
Account: message.From,
Balance: "1000",
Ticker: "TEST",
}),
Spawns: [],
};
case "info":
return {
Assignments: {},
Messages: [],
Output: JSON.stringify({
name: "Test Process",
status: "Running",
version: "1.0.0",
}),
Spawns: [],
};
case "transfer":
return {
Assignments: {},
Messages: [],
Output: JSON.stringify({
From: message.From,
Quantity: message.Tags?.find((t) => t.name === "Quantity")?.value || "0",
Success: true,
To: message.Tags?.find((t) => t.name === "Recipient")?.value ||
"unknown",
}),
Spawns: [],
};
default:
return {
Assignments: {},
Messages: [],
Output: JSON.stringify({
Error: `Unknown action: ${action}`,
}),
Spawns: [],
};
}
};
const getNestedProperty = (obj, path) => {
return path.split(".").reduce((current, key) => current?.[key], obj);
};
export const createAOLiteTestService = (aoMessageService, processService) => service(aoMessageService, processService);