Update customAction and add tests

- Change temp subdirectory from 'devchat/context' to 'devchat/action'.
- Add check for tempFile existence before unlinking.
- Add new test cases for actionManager and customAction.
This commit is contained in:
bobo.yang 2023-07-24 00:11:56 +08:00
parent 3363250305
commit 944e4e0c9b
4 changed files with 239 additions and 29 deletions

View File

@ -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
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
return result;
},
};

View File

@ -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('');
});
});

View File

@ -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: '' });
});
});

View File

@ -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!'