local-agent
Version:
A CLI agentic system for orchestrating tools and memory with per-folder scoping
273 lines • 11.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// Mock all external dependencies BEFORE importing anything
jest.mock('readline');
jest.mock('ai');
jest.mock('@agentic/ai-sdk');
jest.mock('@ai-sdk/openai');
jest.mock('@ai-sdk/anthropic');
jest.mock('@ai-sdk/google');
jest.mock('../src/memory');
jest.mock('marked');
jest.mock('marked-terminal');
// Mock OpenRouter module
jest.mock('@openrouter/ai-sdk-provider', () => ({
createOpenRouter: jest.fn(() => ({
chat: jest.fn()
}))
}), { virtual: true });
const readline = __importStar(require("readline"));
const memory = __importStar(require("../src/memory"));
const mockReadline = readline;
const mockMemory = memory;
// Since we can't easily test the actual interactions module due to ES module issues,
// we'll test the core concepts and functionality patterns
describe('interactions.ts', () => {
let mockRl;
beforeEach(() => {
jest.clearAllMocks();
// Setup mock readline interface
mockRl = {
prompt: jest.fn(),
on: jest.fn(),
write: jest.fn(),
close: jest.fn(),
question: jest.fn()
};
mockReadline.createInterface.mockReturnValue(mockRl);
// Mock console methods
jest.spyOn(console, 'log').mockImplementation();
jest.spyOn(process.stdout, 'write').mockImplementation();
// Mock process.stdout methods - add them if they don't exist
process.stdout.clearLine = jest.fn();
process.stdout.cursorTo = jest.fn();
});
describe('Readline interface setup', () => {
it('should be able to create readline interface', () => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '$> '
});
expect(mockReadline.createInterface).toHaveBeenCalledWith({
input: process.stdin,
output: process.stdout,
prompt: '$> '
});
});
it('should set up event listeners', () => {
const rl = mockReadline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('line', jest.fn());
rl.on('close', jest.fn());
expect(mockRl.on).toHaveBeenCalledWith('line', expect.any(Function));
expect(mockRl.on).toHaveBeenCalledWith('close', expect.any(Function));
});
});
describe('Model string parsing logic', () => {
it('should parse model strings correctly', () => {
const testCases = [
{ input: 'openai/gpt-4', expected: { provider: 'openai', modelName: 'gpt-4' } },
{ input: 'anthropic/claude-3-sonnet', expected: { provider: 'anthropic', modelName: 'claude-3-sonnet' } },
{ input: 'google/gemini-pro', expected: { provider: 'google', modelName: 'gemini-pro' } },
{ input: 'gpt-4', expected: { provider: 'openai', modelName: 'gpt-4' } } // backward compatibility
];
testCases.forEach(testCase => {
let provider, modelName;
if (!testCase.input.includes('/')) {
// Backward compatibility: treat as openai
provider = 'openai';
modelName = testCase.input;
}
else {
const [providerPart, ...rest] = testCase.input.split('/');
provider = providerPart.toLowerCase();
modelName = rest.join('/');
}
expect(provider).toBe(testCase.expected.provider);
expect(modelName).toBe(testCase.expected.modelName);
});
});
});
describe('Multi-step task detection logic', () => {
it('should identify multi-step keywords', () => {
const multiStepKeywords = [
'create', 'build', 'setup', 'make', 'develop', 'implement', 'generate',
'react app', 'project', 'application', 'website', 'api', 'server',
'all files', 'complete', 'full', 'entire', 'whole'
];
const testPrompts = [
'create a React app',
'build a website',
'setup a project',
'make a complete application',
'develop an API server'
];
testPrompts.forEach(prompt => {
const lowerPrompt = prompt.toLowerCase();
const isMultiStep = multiStepKeywords.some(keyword => lowerPrompt.includes(keyword));
expect(isMultiStep).toBe(true);
});
});
it('should not identify single-step tasks', () => {
const singleStepPrompts = [
'what is the weather?',
'list files',
'show me the time',
'help me understand'
];
const multiStepKeywords = [
'create', 'build', 'setup', 'make', 'develop', 'implement', 'generate',
'react app', 'project', 'application', 'website', 'api', 'server',
'all files', 'complete', 'full', 'entire', 'whole'
];
singleStepPrompts.forEach(prompt => {
const lowerPrompt = prompt.toLowerCase();
const isMultiStep = multiStepKeywords.some(keyword => lowerPrompt.includes(keyword));
expect(isMultiStep).toBe(false);
});
});
});
describe('Task continuation logic', () => {
it('should detect continuation indicators', () => {
const continuationIndicators = [
'created folder', 'created directory', 'made folder',
'next step', 'continue', 'now i will', 'now i need to',
'partially complete', 'still need', 'remaining',
'created basic', 'initial setup', 'first step'
];
const continuationResponses = [
'I created folder structure, now I will add the files',
'Created basic setup, next step is configuration',
'Made folder for the project, still need to add dependencies'
];
continuationResponses.forEach(response => {
const lowerResponse = response.toLowerCase();
const shouldContinue = continuationIndicators.some(indicator => lowerResponse.includes(indicator));
expect(shouldContinue).toBe(true);
});
});
it('should detect completion indicators', () => {
const completionIndicators = [
'task complete', 'finished', 'done', 'ready to use',
'fully functional', 'all files created', 'project is complete',
'successfully created', 'everything is set up',
'application is now ready', 'setup is complete'
];
const completionResponses = [
'Task complete! The application is ready to use.',
'All files created successfully. Project is done.',
'Setup is complete and everything is working.'
];
completionResponses.forEach(response => {
const lowerResponse = response.toLowerCase();
const isComplete = completionIndicators.some(indicator => lowerResponse.includes(indicator));
expect(isComplete).toBe(true);
});
});
});
describe('Memory integration', () => {
it('should use memory functions for logging', () => {
// Verify that memory functions are available and properly mocked
expect(mockMemory.logUserPrompt).toBeDefined();
expect(mockMemory.logToolUsed).toBeDefined();
expect(mockMemory.logAgentResponse).toBeDefined();
expect(mockMemory.logAgentError).toBeDefined();
expect(mockMemory.createSessionFile).toBeDefined();
});
});
describe('Provider support', () => {
it('should support multiple AI providers', () => {
const supportedProviders = ['openai', 'anthropic', 'google', 'openrouter'];
supportedProviders.forEach(provider => {
expect(typeof provider).toBe('string');
expect(provider.length).toBeGreaterThan(0);
});
});
});
describe('Error handling patterns', () => {
it('should handle empty prompts', () => {
const emptyPrompts = ['', ' ', '\t', '\n'];
emptyPrompts.forEach(prompt => {
const trimmed = prompt.trim();
expect(trimmed).toBe('');
});
});
it('should handle configuration errors', () => {
const invalidConfigs = [null, undefined, {}];
invalidConfigs.forEach(config => {
if (!config || typeof config !== 'object') {
expect(config).toBeFalsy();
}
});
});
});
describe('UI elements', () => {
it('should handle colored output', () => {
const colors = {
BLUE: '\x1b[34m',
YELLOW: '\x1b[33m',
RESET: '\x1b[0m'
};
Object.values(colors).forEach(color => {
expect(typeof color).toBe('string');
expect(color).toMatch(/\x1b\[\d+m/);
});
});
it('should handle spinner frames', () => {
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
expect(spinnerFrames).toHaveLength(10);
spinnerFrames.forEach(frame => {
expect(typeof frame).toBe('string');
expect(frame.length).toBe(1);
});
});
});
describe('Session management', () => {
it('should handle session cleanup', () => {
const mockClose = jest.fn();
const mockInterface = {
close: mockClose,
on: jest.fn()
};
mockInterface.close();
expect(mockClose).toHaveBeenCalled();
});
});
});
//# sourceMappingURL=interactions.test.js.map