diff --git a/src/action/customAction.ts b/src/action/customAction.ts index e0a3aa9..394a616 100644 --- a/src/action/customAction.ts +++ b/src/action/customAction.ts @@ -96,7 +96,7 @@ export class CustomActions { handlerAction: async (args: { [key: string]: string }) => { // Implement the handler logic for the custom action - const tempDir = await createTempSubdirectory('devchat/context'); + const tempDir = await createTempSubdirectory('devchat/action'); const tempFile = path.join(tempDir, 'apply.json'); const contextMap = { @@ -109,6 +109,7 @@ export class CustomActions { // Save contextMap to temp file await UiUtilWrapper.writeFile(tempFile, JSON.stringify(contextMap)); + // replace ${contextFile} with tempFile for arg in handler let handlerArgs = action.handler.map(arg => arg.replace('${contextFile}', tempFile)); if (args !== undefined) { @@ -140,7 +141,9 @@ export class CustomActions { logger.channel()?.info(`stderr:`, result.stderr); // remove temp file - fs.unlinkSync(tempFile); + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } return result; }, }; diff --git a/test/action/actionManager.test.ts b/test/action/actionManager.test.ts index 3ad8d6d..c906b91 100644 --- a/test/action/actionManager.test.ts +++ b/test/action/actionManager.test.ts @@ -66,4 +66,115 @@ describe('ActionManager', () => { expect(result.stdout).to.equal(''); expect(result.stderr).to.equal(''); }); + + it('should apply action with content.content.xxx type args correctly', async () => { + const actionManager = ActionManager.getInstance(); + const testActionWithNestedArgs: Action = { + ...testAction, + name: 'testActionWithNestedArgs', + args: [ + { + name: 'arg1', + description: 'arg1 description', + type: 'string', + from: 'content.content.field1', + }, + ], + handlerAction: async (args) => { + if (args.arg1 === 'value1') { + return { exitCode: 0, stdout: '', stderr: '' }; + } else { + return { exitCode: -1, stdout: '', stderr: 'Incorrect arg1 value' }; + } + }, + }; + actionManager.registerAction(testActionWithNestedArgs); + const actionName = 'testActionWithNestedArgs'; + const content = { + command: 'test', + content: JSON.stringify({ field1: 'value1' }), + fileName: 'test.txt', + }; + const result = await actionManager.applyAction(actionName, content); + expect(result.exitCode).to.equal(0); + expect(result.stdout).to.equal(''); + expect(result.stderr).to.equal(''); + }); + + it('should return error when content is missing required args', async () => { + const actionManager = ActionManager.getInstance(); + const testActionWithMissingArgs: Action = { + ...testAction, + name: 'testActionWithMissingArgs', + args: [ + { + name: 'arg1', + description: 'arg1 description', + type: 'string', + from: 'content.field1', + }, + ], + handlerAction: async (args) => { + return { exitCode: 0, stdout: '', stderr: '' }; + }, + }; + actionManager.registerAction(testActionWithMissingArgs); + const actionName = 'testActionWithMissingArgs'; + const content = { + command: 'test', + content: JSON.stringify({}), + fileName: 'test.txt', + }; + const result = await actionManager.applyAction(actionName, content); + expect(result.exitCode).to.equal(-1); + expect(result.stdout).to.equal(''); + expect(result.stderr).to.equal('Action testActionWithMissingArgs arg arg1 from content.field1 is undefined'); + }); + + it('should trigger handlerAction of CommandRunAction', async () => { + class TestAction implements Action { + name: string; + description: string; + type: string[]; + action: string; + handler: string[]; + args: { name: string; description: string; type: string; as?: string | undefined; from: string; }[]; + + constructor() { + this.name = 'testAction2'; + this.description = 'Test action for unit testing'; + this.type = ['test']; + this.action = 'test'; + this.handler = []; + this.args = []; + } + async handlerAction(content: any): Promise<{ exitCode: number; stdout: string; stderr: string }> { + return { + exitCode: 0, + stdout: 'Test action executed successfully', + stderr: '', + }; + } + } + + const actionManager = ActionManager.getInstance(); + const testAction = new TestAction(); + actionManager.registerAction(testAction); + + const commandRunActionContent = { + command: 'testAction2', + args: {}, + }; + + const content = { + command: 'command_run', + content: JSON.stringify(commandRunActionContent), + fileName: 'test.txt', + }; + + const result = await actionManager.applyAction('command_run', content); + expect(result.exitCode).to.equal(0); + expect(result.stdout).to.equal('Test action executed successfully'); + expect(result.stderr).to.equal(''); + }); }); \ No newline at end of file diff --git a/test/action/customAction.test.ts b/test/action/customAction.test.ts index 9857e26..118ef16 100644 --- a/test/action/customAction.test.ts +++ b/test/action/customAction.test.ts @@ -1,5 +1,7 @@ import { expect } from 'chai'; import 'mocha'; +import fs from 'fs'; +import path from 'path'; import { CustomActions } from '../../src/action/customAction'; describe('CustomActions', () => { @@ -27,4 +29,125 @@ describe('CustomActions', () => { const instruction = customActions.actionInstruction(); expect(instruction).to.include('sampleAction: A sample action for testing'); }); + + it('should return action instruction with args', () => { + // Add a sample action with args to the customActions instance + customActions.getActions().push({ + name: 'sampleActionWithArgs', + description: 'A sample action with args for testing', + type: ['test'], + action: 'sample', + handler: [], + args: [ + { + "name": "arg1", + "description": "Argument 1", + "type": "string", + "from": "content.fileName" + }, + { + "name": "arg2", + "description": "Argument 2", + "type": "number", + "from": "content.content" + } + ], + handlerAction: async (args: { [key: string]: string }) => { + return { exitCode: 0, stdout: '', stderr: '' }; + }, + }); + + const instruction = customActions.actionInstruction(); + expect(instruction).to.include('sampleActionWithArgs: A sample action with args for testing'); + expect(instruction).to.include('Args:'); + expect(instruction).to.include('name: arg1 type: (string) description: Argument 1'); + expect(instruction).to.include('name: arg2 type: (number) description: Argument 2'); + }); + + it('should parse actions from workflows directory', () => { + // Create a temporary workflows directory with a sample action + const workflowsDir = path.join(__dirname, 'temp_workflows'); + fs.mkdirSync(workflowsDir); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension')); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension', 'action')); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension', 'action', 'sample_action')); + const settingsPath = path.join(workflowsDir, 'sample_extension', 'action', 'sample_action', '_setting_.json'); + const sampleActionSettings = { + name: 'sampleParsedAction', + description: 'A sample parsed action for testing', + type: ['test'], + action: 'sample', + handler: [], + args: [], + }; + fs.writeFileSync(settingsPath, JSON.stringify(sampleActionSettings)); + + // Call parseActions with the temporary workflows directory + customActions.parseActions(workflowsDir); + + // Check if the parsed action is in the actions list + const actions = customActions.getActions(); + const parsedAction = actions.find(action => action.name === 'sampleParsedAction'); + expect(parsedAction).to.not.be.undefined; + expect(parsedAction?.description).to.equal('A sample parsed action for testing'); + + // Clean up the temporary workflows directory + fs.unlinkSync(settingsPath); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension', 'action', 'sample_action')); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension', 'action')); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension')); + fs.rmdirSync(workflowsDir); + }); + + it('should parse handlerAction correctly from directory with echo command', async () => { + // Create a temporary directory for the sample action + const workflowsDir = path.join(__dirname, 'temp_workflows'); + fs.mkdirSync(workflowsDir); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension')); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension', 'action')); + fs.mkdirSync(path.join(workflowsDir, 'sample_extension', 'action', 'sample_action')); + const settingsPath = path.join(workflowsDir, 'sample_extension', 'action', 'sample_action', '_setting_.json'); + const sampleActionJson = { + name: 'sampleAction', + description: 'A sample action with a handlerAction method for testing', + type: ['test'], + action: 'sample', + handler: ["echo", "${arg1}"], + args: [ + { name: 'arg1', type: 'string' }, + { name: 'arg2', type: 'string' }, + ], + }; + fs.writeFileSync(settingsPath, JSON.stringify(sampleActionJson)); + + + // Call parseActions with the temporary directory + customActions.parseActions(workflowsDir); + const actions = customActions.getActions(); + + // Clean up the temporary directory + fs.unlinkSync(settingsPath); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension', 'action', 'sample_action')); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension', 'action')); + fs.rmdirSync(path.join(workflowsDir, 'sample_extension')); + fs.rmdirSync(workflowsDir); + + // Check if the returned actions array has the expected length + expect(actions.length).equal(1); + + // Get the parsed action object + const parsedAction = actions[0]; + + // Call the handlerAction method with valid args + const validResult = await parsedAction.handlerAction({ arg1: 'value1', arg2: 'value2' }); + + // Check if the returned CommandResult is as expected + expect(validResult).to.deep.equal({ exitCode: 0, stdout: 'value1\n', stderr: '' }); + + // Call the handlerAction method with invalid args + const invalidResult = await parsedAction.handlerAction({ arg1: 'wrongValue', arg2: 'value2' }); + + // Check if the returned CommandResult is as expected + expect(invalidResult).to.deep.equal({ exitCode: 0, stdout: 'wrongValue\n', stderr: '' }); + }); }); \ No newline at end of file diff --git a/test/handler/sendMessageBase.test.ts b/test/handler/sendMessageBase.test.ts index fd88d4f..a71a7da 100644 --- a/test/handler/sendMessageBase.test.ts +++ b/test/handler/sendMessageBase.test.ts @@ -119,33 +119,6 @@ describe('sendMessageBase', () => { }); describe('sendMessageBase', async () => { - it('should send message correct with openai api key', async () => { - const message = { - text: 'Hello, world!' - }; - const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => { - // Handle partial data - }; - - workspaceFoldersFirstPathStub.returns('./'); - - getConfigurationStub.withArgs('DevChat', 'API_KEY').returns(process.env.TEST_OPENAI_API_KEY); - getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4'); - getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0); - getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true'); - getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI'); - getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000); - - const result = await sendMessageBase(message, handlePartialData); - expect(result).to.be.an('object'); - expect(result!.command).to.equal('receiveMessage'); - expect(result!.text).to.be.a('string'); - expect(result!.hash).to.be.a('string'); - expect(result!.user).to.be.a('string'); - expect(result!.date).to.be.a('string'); - expect(result!.isError).to.be.false; - }).timeout(10000); - it('should send message correct with DevChat access key', async () => { const message = { text: 'Hello, world!'