Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions backend/__tests__/service/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ describe('Auth Routes Integration Tests', () => {
const token = generateTestToken(user._id);

const updateData = {
profilePicture: 'new-profile-pic-url',
profilePicture: 'https://api-dev.commonly.me/api/uploads/new-profile-pic.png',
};

const response = await request(app)
Expand All @@ -426,11 +426,11 @@ describe('Auth Routes Integration Tests', () => {
.send(updateData)
.expect(200);

expect(response.body.profilePicture).toBe('new-profile-pic-url');
expect(response.body.profilePicture).toBe('/api/uploads/new-profile-pic.png');

// Verify database update
const updatedUser = await User.findById(user._id);
expect(updatedUser.profilePicture).toBe('new-profile-pic-url');
expect(updatedUser.profilePicture).toBe('/api/uploads/new-profile-pic.png');
});

it('should return 401 without token', async () => {
Expand Down
4 changes: 2 additions & 2 deletions backend/__tests__/service/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ describe('User Routes Integration Tests', () => {
.send({ profilePicture: 'newpic.jpg' })
.expect(200);

expect(res.body.profilePicture).toBe('newpic.jpg');
expect(res.body.profilePicture).toBe('/api/uploads/newpic.jpg');

const updated = await User.findById(user._id);
expect(updated.profilePicture).toBe('newpic.jpg');
expect(updated.profilePicture).toBe('/api/uploads/newpic.jpg');
});
});
7 changes: 4 additions & 3 deletions backend/__tests__/unit/controllers/authController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ describe('Auth Controller Tests', () => {
_id: userId,
username: 'testuser',
email: 'test@example.com',
profilePicture: 'new-profile-pic-url',
profilePicture: '/api/uploads/new-profile-pic.png',
};

// First mock for the initial findById
Expand All @@ -573,7 +573,7 @@ describe('Auth Controller Tests', () => {
const req = {
userId,
body: {
profilePicture: 'new-profile-pic-url',
profilePicture: 'https://api.commonly.me/api/uploads/new-profile-pic.png',
},
};

Expand All @@ -587,12 +587,13 @@ describe('Auth Controller Tests', () => {
// Verify response
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
profilePicture: 'new-profile-pic-url',
profilePicture: '/api/uploads/new-profile-pic.png',
}),
);

// Verify save was called
expect(initialUser.save).toHaveBeenCalled();
expect(initialUser.profilePicture).toBe('/api/uploads/new-profile-pic.png');
});

it('should return 404 if user not found', async () => {
Expand Down
14 changes: 10 additions & 4 deletions backend/__tests__/unit/controllers/userController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ describe('User Controller', () => {

describe('updateProfile', () => {
it('updates the profile picture of the user', async () => {
const updatedUser = mockUserDoc({ _id: 'u1', profilePicture: 'newpic' });
const updatedUser = mockUserDoc({ _id: 'u1', profilePicture: '/api/uploads/newpic.png' });
User.findByIdAndUpdate = jest
.fn()
.mockReturnValue({ select: jest.fn().mockResolvedValueOnce(updatedUser) });

const req = { user: { id: 'u1' }, body: { profilePicture: 'newpic' } };
const req = {
user: { id: 'u1' },
body: { profilePicture: 'https://api-dev.commonly.me/api/uploads/newpic.png' },
};
const res = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
Expand All @@ -63,10 +66,13 @@ describe('User Controller', () => {
await userController.updateProfile(req, res);
expect(User.findByIdAndUpdate).toHaveBeenCalledWith(
'u1',
{ $set: { profilePicture: 'newpic' } },
{ $set: { profilePicture: '/api/uploads/newpic.png' } },
{ new: true },
);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ _id: 'u1', profilePicture: 'newpic' }));
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
_id: 'u1',
profilePicture: '/api/uploads/newpic.png',
}));
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ jest.mock('../../../models/AgentProfile', () => ({
findOneAndUpdate: jest.fn(),
}));

jest.mock('../../../models/AgentTemplate', () => ({
find: jest.fn(),
}));

jest.mock('../../../models/Activity', () => ({
create: jest.fn(),
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jest.mock('../../../models/AgentProfile', () => ({
findOneAndUpdate: jest.fn(),
}));

jest.mock('../../../models/AgentTemplate', () => ({
find: jest.fn(),
}));

jest.mock('../../../models/Activity', () => ({
create: jest.fn(),
}));
Expand Down Expand Up @@ -59,6 +63,7 @@ const { AgentRegistry, AgentInstallation } = require('../../../models/AgentRegis
const Pod = require('../../../models/Pod');
const User = require('../../../models/User');
const AgentProfile = require('../../../models/AgentProfile');
const AgentTemplate = require('../../../models/AgentTemplate');
const Activity = require('../../../models/Activity');
const AgentIdentityService = require('../../../services/agentIdentityService');
const FirstContactService = require('../../../services/firstContactService');
Expand Down Expand Up @@ -120,6 +125,11 @@ describe('registry install runtimeType fallback', () => {
User.findById.mockReturnValue(buildSelectLeanChain({ username: 'installer', role: 'admin' }));

AgentProfile.findOneAndUpdate.mockResolvedValue(true);
AgentTemplate.find.mockReturnValue({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue([]),
}),
});
Activity.create.mockResolvedValue(true);
});

Expand Down Expand Up @@ -254,4 +264,54 @@ describe('registry install runtimeType fallback', () => {
expect(res.status).not.toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: true }));
});

it('seeds a new identity from the normalized per-instance template icon', async () => {
AgentRegistry.getByName.mockResolvedValue({
agentName: 'sample-agent',
displayName: 'Sample Agent',
description: 'Community marketplace app',
iconUrl: 'https://api.commonly.me/api/uploads/registry.png',
latestVersion: '1.0.0',
manifest: {
context: { required: [] },
runtime: { type: 'standalone' },
},
});
AgentTemplate.find.mockReturnValue({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue([{
displayName: 'Aria',
iconUrl: 'https://api-dev.commonly.me/api/uploads/aria.png',
createdBy: 'user-1',
visibility: 'private',
}]),
}),
});
const req = {
body: {
agentName: 'sample-agent',
podId: 'pod-1',
version: '1.0.0',
displayName: 'Aria',
config: {},
scopes: [],
},
user: { id: 'user-1', username: 'installer' },
userId: 'user-1',
};
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};

await installHandler(req, res);

expect(AgentIdentityService.getOrCreateAgentUser).toHaveBeenCalledWith(
'sample-agent',
expect.objectContaining({
displayName: 'Aria',
profilePicture: '/api/uploads/aria.png',
}),
);
});
});
92 changes: 92 additions & 0 deletions backend/__tests__/unit/routes/registry.templates-avatar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
jest.mock('../../../models/AgentRegistry', () => ({
AgentRegistry: {
getByName: jest.fn(),
},
}));

jest.mock('../../../models/AgentTemplate', () => ({
find: jest.fn(),
findOne: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
deleteOne: jest.fn(),
}));

const { AgentRegistry } = require('../../../models/AgentRegistry');
const AgentTemplate = require('../../../models/AgentTemplate');
const templatesRouter = require('../../../routes/registry/templates');

const getHandler = (method, path) => {
const layer = templatesRouter.stack.find((entry) => (
entry.route && entry.route.path === path && entry.route.methods[method]
));
if (!layer) throw new Error(`${method.toUpperCase()} ${path} handler not found`);
return layer.route.stack[layer.route.stack.length - 1].handle;
};

const response = () => ({
status: jest.fn().mockReturnThis(),
json: jest.fn(),
});

describe('registry template avatar writes', () => {
beforeEach(() => {
jest.clearAllMocks();
AgentRegistry.getByName.mockResolvedValue({ agentName: 'openclaw' });
AgentTemplate.findOne.mockReturnValue({
select: jest.fn().mockReturnValue({
lean: jest.fn().mockResolvedValue(null),
}),
});
AgentTemplate.create.mockImplementation(async (data) => ({
...data,
_id: { toString: () => 'template-1' },
}));
});

it('stores a newly created package icon as a relative upload URL', async () => {
const req = {
userId: 'user-1',
body: {
agentName: 'openclaw',
displayName: 'Aria',
iconUrl: 'https://api-dev.commonly.me/api/uploads/aria.png',
},
};
const res = response();

await getHandler('post', '/templates')(req, res);

expect(AgentTemplate.create).toHaveBeenCalledWith(expect.objectContaining({
iconUrl: '/api/uploads/aria.png',
}));
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
template: expect.objectContaining({ iconUrl: '/api/uploads/aria.png' }),
}));
});

it('normalizes an updated package icon without mutating identity data', async () => {
const template = {
_id: 'template-1',
agentName: 'openclaw',
displayName: 'Aria',
description: '',
iconUrl: '/api/uploads/old.png',
visibility: 'private',
createdBy: { toString: () => 'user-1' },
save: jest.fn().mockResolvedValue(undefined),
};
AgentTemplate.findById.mockResolvedValue(template);
const req = {
userId: 'user-1',
params: { id: 'template-1' },
body: { iconUrl: 'http://localhost:5000/api/uploads/new.png' },
};
const res = response();

await getHandler('patch', '/templates/:id')(req, res);

expect(template.iconUrl).toBe('/api/uploads/new.png');
expect(template.save).toHaveBeenCalled();
});
});
4 changes: 3 additions & 1 deletion backend/__tests__/unit/routes/uploads.post.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('uploads POST / (ADR-002 Phase 1)', () => {
});

it('writes bytes through the driver and saves metadata-only File', async () => {
await request(app)
const res = await request(app)
.post('/api/uploads')
.attach('image', Buffer.from('data'), 'photo.png')
.expect(200);
Expand All @@ -64,6 +64,8 @@ describe('uploads POST / (ADR-002 Phase 1)', () => {
expect(fileArgs.contentType).toBe('image/png');
expect(fileArgs.uploadedBy).toBe('user1');
expect(File.__saveMock).toHaveBeenCalled();
expect(res.body.url).toMatch(/^\/api\/uploads\/[^/]+\.png$/);
expect(res.body.url).not.toMatch(/^https?:\/\//);
});

it('returns 400 when no file is provided', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,39 @@ describe('inline displayName collision resolver (sticky dedup)', () => {
expect(mockSaved[0].botMetadata.displayName).toBe('Pixel');
});

test('new identity seeds a normalized package avatar', async () => {
await AgentIdentityService.getOrCreateAgentUser('openclaw', {
instanceId: 'aria',
displayName: 'Aria',
profilePicture: 'https://api-dev.commonly.me/api/uploads/aria.png',
});

expect(mockSaved[0].profilePicture).toBe('/api/uploads/aria.png');
});

test('reinstall does not overwrite an existing customized identity avatar', async () => {
mockExisting = {
_id: 'aria-id',
username: 'openclaw-aria',
profilePicture: '/api/uploads/customized.png',
isBot: true,
botMetadata: {
agentName: 'openclaw',
instanceId: 'aria',
displayName: 'Aria',
runtime: 'moltbot',
},
};

const user = await AgentIdentityService.getOrCreateAgentUser('openclaw', {
instanceId: 'aria',
profilePicture: '/api/uploads/package-seed.png',
});

expect(user.profilePicture).toBe('/api/uploads/customized.png');
expect(mockSaved).toHaveLength(0);
});

test('new install collides with an existing canonical — gets suffix', async () => {
// openclaw-pixel already has displayName="Pixel" with instanceId "pixel" (shorter)
mockPeers = [
Expand Down
Loading
Loading