👤 feat: Enhance Agent Versioning to Track User Updates (#7523)

* feat: Enhance agent update functionality to track user updates

- Updated `updateAgent` function to accept an `updatingUserId` parameter for tracking who made changes.
- Modified agent versioning to include `updatedBy` field for better audit trails.
- Adjusted related functions and tests to ensure proper handling of user updates and version history.
- Enhanced tests to verify correct tracking of `updatedBy` during agent updates and restorations.

* fix: Refactor import tests for improved readability and consistency

- Adjusted formatting in `importChatGptConvo` test to enhance clarity.
- Updated expected output string in `processAssistantMessage` test to use double quotes for consistency.
- Modified processing time expectation in `processAssistantMessage` test to allow for CI environment variability.
This commit is contained in:
matt burnett 2025-05-23 20:47:14 -04:00 committed by GitHub
parent ed9ab8842a
commit cede5d120c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 172 additions and 22 deletions

View file

@ -21,7 +21,7 @@ const Agent = mongoose.model('agent', agentSchema);
* @throws {Error} If the agent creation fails.
*/
const createAgent = async (agentData) => {
const { versions, ...versionData } = agentData;
const { author, ...versionData } = agentData;
const timestamp = new Date();
const initialAgentData = {
...agentData,
@ -163,6 +163,7 @@ const isDuplicateVersion = (updateData, currentData, versions) => {
'createdAt',
'updatedAt',
'author',
'updatedBy',
'created_at',
'updated_at',
'__v',
@ -248,15 +249,16 @@ const isDuplicateVersion = (updateData, currentData, versions) => {
* @param {string} searchParameter.id - The ID of the agent to update.
* @param {string} [searchParameter.author] - The user ID of the agent's author.
* @param {Object} updateData - An object containing the properties to update.
* @param {string} [updatingUserId] - The ID of the user performing the update (used for tracking non-author updates).
* @returns {Promise<Agent>} The updated or newly created agent document as a plain object.
* @throws {Error} If the update would create a duplicate version
*/
const updateAgent = async (searchParameter, updateData) => {
const updateAgent = async (searchParameter, updateData, updatingUserId = null) => {
const options = { new: true, upsert: false };
const currentAgent = await Agent.findOne(searchParameter);
if (currentAgent) {
const { __v, _id, id, versions, ...versionData } = currentAgent.toObject();
const { __v, _id, id, versions, author, ...versionData } = currentAgent.toObject();
const { $push, $pull, $addToSet, ...directUpdates } = updateData;
if (Object.keys(directUpdates).length > 0 && versions && versions.length > 0) {
@ -276,13 +278,20 @@ const updateAgent = async (searchParameter, updateData) => {
}
}
const versionEntry = {
...versionData,
...directUpdates,
updatedAt: new Date(),
};
// Always store updatedBy field to track who made the change
if (updatingUserId) {
versionEntry.updatedBy = new mongoose.Types.ObjectId(updatingUserId);
}
updateData.$push = {
...($push || {}),
versions: {
...versionData,
...directUpdates,
updatedAt: new Date(),
},
versions: versionEntry,
};
}
@ -298,7 +307,7 @@ const updateAgent = async (searchParameter, updateData) => {
* @param {string} params.file_id
* @returns {Promise<Agent>} The updated agent.
*/
const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
const addAgentResourceFile = async ({ req, agent_id, tool_resource, file_id }) => {
const searchParameter = { id: agent_id };
let agent = await getAgent(searchParameter);
if (!agent) {
@ -324,7 +333,7 @@ const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
},
};
const updatedAgent = await updateAgent(searchParameter, updateData);
const updatedAgent = await updateAgent(searchParameter, updateData, req?.user?.id);
if (updatedAgent) {
return updatedAgent;
} else {
@ -488,7 +497,7 @@ const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds
delete updateQuery.author;
}
const updatedAgent = await updateAgent(updateQuery, updateOps);
const updatedAgent = await updateAgent(updateQuery, updateOps, user.id);
if (updatedAgent) {
return updatedAgent;
}
@ -533,6 +542,8 @@ const revertAgentVersion = async (searchParameter, versionIndex) => {
delete updateData._id;
delete updateData.id;
delete updateData.versions;
delete updateData.author;
delete updateData.updatedBy;
return Agent.findOneAndUpdate(searchParameter, updateData, { new: true }).lean();
};

View file

@ -679,7 +679,7 @@ describe('Agent Version History', () => {
expect(updatedAgent.versions[0]._id).toBeUndefined();
expect(updatedAgent.versions[0].__v).toBeUndefined();
expect(updatedAgent.versions[0].name).toBe('Test Agent');
expect(updatedAgent.versions[0].author).toBeDefined();
expect(updatedAgent.versions[0].author).toBeUndefined();
expect(updatedAgent.versions[1]._id).toBeUndefined();
expect(updatedAgent.versions[1].__v).toBeUndefined();
@ -885,4 +885,141 @@ describe('Agent Version History', () => {
console.error = originalConsoleError;
}
});
test('should track updatedBy when a different user updates an agent', async () => {
const agentId = `agent_${uuidv4()}`;
const originalAuthor = new mongoose.Types.ObjectId();
const updatingUser = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Original Agent',
provider: 'test',
model: 'test-model',
author: originalAuthor,
description: 'Original description',
});
const updatedAgent = await updateAgent(
{ id: agentId },
{ name: 'Updated Agent', description: 'Updated description' },
updatingUser.toString(),
);
expect(updatedAgent.versions).toHaveLength(2);
expect(updatedAgent.versions[1].updatedBy.toString()).toBe(updatingUser.toString());
expect(updatedAgent.author.toString()).toBe(originalAuthor.toString());
});
test('should include updatedBy even when the original author updates the agent', async () => {
const agentId = `agent_${uuidv4()}`;
const originalAuthor = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Original Agent',
provider: 'test',
model: 'test-model',
author: originalAuthor,
description: 'Original description',
});
const updatedAgent = await updateAgent(
{ id: agentId },
{ name: 'Updated Agent', description: 'Updated description' },
originalAuthor.toString(),
);
expect(updatedAgent.versions).toHaveLength(2);
expect(updatedAgent.versions[1].updatedBy.toString()).toBe(originalAuthor.toString());
expect(updatedAgent.author.toString()).toBe(originalAuthor.toString());
});
test('should track multiple different users updating the same agent', async () => {
const agentId = `agent_${uuidv4()}`;
const originalAuthor = new mongoose.Types.ObjectId();
const user1 = new mongoose.Types.ObjectId();
const user2 = new mongoose.Types.ObjectId();
const user3 = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Original Agent',
provider: 'test',
model: 'test-model',
author: originalAuthor,
description: 'Original description',
});
// User 1 makes an update
await updateAgent(
{ id: agentId },
{ name: 'Updated by User 1', description: 'First update' },
user1.toString(),
);
// Original author makes an update
await updateAgent(
{ id: agentId },
{ description: 'Updated by original author' },
originalAuthor.toString(),
);
// User 2 makes an update
await updateAgent(
{ id: agentId },
{ name: 'Updated by User 2', model: 'new-model' },
user2.toString(),
);
// User 3 makes an update
const finalAgent = await updateAgent(
{ id: agentId },
{ description: 'Final update by User 3' },
user3.toString(),
);
expect(finalAgent.versions).toHaveLength(5);
expect(finalAgent.author.toString()).toBe(originalAuthor.toString());
// Check that each version has the correct updatedBy
expect(finalAgent.versions[0].updatedBy).toBeUndefined(); // Initial creation has no updatedBy
expect(finalAgent.versions[1].updatedBy.toString()).toBe(user1.toString());
expect(finalAgent.versions[2].updatedBy.toString()).toBe(originalAuthor.toString());
expect(finalAgent.versions[3].updatedBy.toString()).toBe(user2.toString());
expect(finalAgent.versions[4].updatedBy.toString()).toBe(user3.toString());
// Verify the final state
expect(finalAgent.name).toBe('Updated by User 2');
expect(finalAgent.description).toBe('Final update by User 3');
expect(finalAgent.model).toBe('new-model');
});
test('should preserve original author during agent restoration', async () => {
const agentId = `agent_${uuidv4()}`;
const originalAuthor = new mongoose.Types.ObjectId();
const updatingUser = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Original Agent',
provider: 'test',
model: 'test-model',
author: originalAuthor,
description: 'Original description',
});
await updateAgent(
{ id: agentId },
{ name: 'Updated Agent', description: 'Updated description' },
updatingUser.toString(),
);
const { revertAgentVersion } = require('./Agent');
const revertedAgent = await revertAgentVersion({ id: agentId }, 0);
expect(revertedAgent.author.toString()).toBe(originalAuthor.toString());
expect(revertedAgent.name).toBe('Original Agent');
expect(revertedAgent.description).toBe('Original description');
});
});