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
11 changes: 11 additions & 0 deletions backend/__tests__/service/agent-manage-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,23 @@ describe('per-installation agent settings', () => {
expect(response.body.activePodCount).toBe(2);
expect(response.body.podCount).toBe(2);
expect(response.body.turnCap).toBe(30);
expect(response.body.userRunCeiling).toBe(120);
expect(response.body.writablePodId).toBe(String(podA._id));
const otherResponse = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(other)}`);
expect(otherResponse.body.writablePodId).toBe(String(podB._id));
});

it('shows the installation owner’s quota when an admin opens settings', async () => {
await User.updateOne({ _id: owner._id }, { $set: { 'entitlements.pro': true } });
const response = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
.set('Authorization', `Bearer ${token(admin)}`);
expect(response.status).toBe(200);
expect(response.body.turnCap).toBe(200);
expect(response.body.userRunCeiling).toBe(300);
await User.updateOne({ _id: owner._id }, { $set: { 'entitlements.pro': false } });
});

it('counts active connections separately from installed pod rows', async () => {
await AgentInstallation.updateOne({ podId: podB._id, agentName: 'settings-agent' }, { status: 'paused' });
const response = await request(app).get('/api/registry/agents/settings-agent/manage-settings?instanceId=same')
Expand Down
141 changes: 141 additions & 0 deletions backend/__tests__/service/agent-memory-editor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
const express = require('express');
const request = require('supertest');
const jwt = require('jsonwebtoken');
const { setupMongoDb, closeMongoDb } = require('../utils/testUtils');
const User = require('../../models/User');
const Pod = require('../../models/Pod');
const AgentMemory = require('../../models/AgentMemory');
const { AgentInstallation } = require('../../models/AgentRegistry');
const memoryView = require('../../routes/agentMemoryView');

const SECRET = 'agent-memory-editor-test';
const token = (user) => jwt.sign({ id: String(user._id) }, SECRET);
const base = '/api/agent-memory/editor-agent/machine-1/editable';

describe('owner memory editor', () => {
let app; let owner; let outsider; let admin; let pod;

beforeAll(async () => {
process.env.JWT_SECRET = SECRET;
await setupMongoDb();
app = express();
app.use(express.json());
app.use('/api/agent-memory', memoryView);
owner = await User.create({ username: 'memory-edit-owner', email: 'memory-edit-owner@test.com', password: 'password123' });
outsider = await User.create({ username: 'memory-edit-outsider', email: 'memory-edit-outsider@test.com', password: 'password123' });
admin = await User.create({ username: 'memory-edit-admin', email: 'memory-edit-admin@test.com', password: 'password123', role: 'admin' });
pod = await Pod.create({ name: 'Editor pod', type: 'chat', createdBy: owner._id, members: [owner._id, outsider._id] });
});

afterAll(async () => closeMongoDb());

beforeEach(async () => {
await AgentInstallation.deleteMany({ agentName: 'editor-agent' });
await AgentMemory.deleteMany({ agentName: 'editor-agent' });
await AgentInstallation.create({ agentName: 'editor-agent', instanceId: 'machine-1', podId: pod._id,
version: '1', installedBy: owner._id });
await AgentMemory.create({ agentName: 'editor-agent', instanceId: 'machine-1',
content: 'Old note', sections: {
long_term: { content: 'Old note', visibility: 'private', updatedAt: new Date('2026-01-01'), byteSize: 8 },
dedup_state: { content: 'internal key', visibility: 'private', byteSize: 12 },
} });
});

it('shows editable documents only to the installer or admin', async () => {
const ownerRead = await request(app).get(base).set('Authorization', `Bearer ${token(owner)}`);
const adminRead = await request(app).get(base).set('Authorization', `Bearer ${token(admin)}`);
const outsiderRead = await request(app).get(base).set('Authorization', `Bearer ${token(outsider)}`);
expect(ownerRead.status).toBe(200);
expect(adminRead.status).toBe(200);
expect(outsiderRead.status).toBe(403);
expect(ownerRead.body.sections.long_term.content).toBe('Old note');
expect(JSON.stringify(ownerRead.body)).not.toContain('internal key');
});

it('preserves sibling memory and rejects a stale edit', async () => {
const read = await request(app).get(base).set('Authorization', `Bearer ${token(owner)}`);
const revision = read.body.sections.long_term.updatedAt;
const saved = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'New note', expectedUpdatedAt: revision });
expect(saved.status).toBe(200);
const stale = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'Old edit', expectedUpdatedAt: revision });
expect(stale.status).toBe(409);
const record = await AgentMemory.findOne({ agentName: 'editor-agent', instanceId: 'machine-1' }).lean();
expect(record.content).toBe('New note');
expect(record.sections.long_term.content).toBe('New note');
expect(record.sections.dedup_state.content).toBe('internal key');
expect(record.sections.long_term.history[0].content).toBe('Old note');
});

it('creates the first memory document and rejects a stale second creation', async () => {
await AgentMemory.deleteMany({ agentName: 'editor-agent' });
const first = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'First note', expectedUpdatedAt: null });
expect(first.status).toBe(200);
const stale = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'Stale note', expectedUpdatedAt: null });
expect(stale.status).toBe(409);
const records = await AgentMemory.find({ agentName: 'editor-agent', instanceId: 'machine-1' }).lean();
expect(records).toHaveLength(1);
expect(records[0].content).toBe('First note');
});

it('shows legacy long-term content and keeps it in history on the first edit', async () => {
await AgentMemory.updateOne({ agentName: 'editor-agent', instanceId: 'machine-1' },
{ $set: { content: 'Legacy note' }, $unset: { 'sections.long_term': 1 } });
const read = await request(app).get(base).set('Authorization', `Bearer ${token(owner)}`);
expect(read.body.sections.long_term).toMatchObject({ content: 'Legacy note', updatedAt: null });
const saved = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'New note', expectedUpdatedAt: null });
expect(saved.status).toBe(200);
const record = await AgentMemory.findOne({ agentName: 'editor-agent', instanceId: 'machine-1' }).lean();
expect(record.sections.long_term.history[0].content).toBe('Legacy note');
});

it('blocks another member from editing and refuses unknown sections', async () => {
const denied = await request(app).patch(`${base}/long_term`).set('Authorization', `Bearer ${token(outsider)}`)
.send({ content: 'No', expectedUpdatedAt: null });
const unknown = await request(app).patch(`${base}/dedup_state`).set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'No', expectedUpdatedAt: null });
expect(denied.status).toBe(403);
expect(unknown.status).toBe(404);
});

it('keeps the editor available to the installer while the agent is paused', async () => {
await AgentInstallation.updateMany({ agentName: 'editor-agent' }, { $set: { status: 'paused' } });
const response = await request(app).get(base).set('Authorization', `Bearer ${token(owner)}`);
const index = await request(app).get('/api/agent-memory/editor-agent/machine-1')
.set('Authorization', `Bearer ${token(owner)}`);
expect(response.status).toBe(200);
expect(response.body.sections.long_term.content).toBe('Old note');
expect(index.status).toBe(200);
expect(index.body.pods).toHaveLength(1);
});

it('denies a paused installer when another installer owns the same identity', async () => {
const otherPod = await Pod.create({ name: 'Other owner pod', type: 'chat',
createdBy: outsider._id, members: [outsider._id] });
await AgentInstallation.updateMany({ agentName: 'editor-agent' }, { $set: { status: 'paused' } });
await AgentInstallation.create({ agentName: 'editor-agent', instanceId: 'machine-1',
podId: otherPod._id, version: '1', installedBy: outsider._id });

const read = await request(app).get(base).set('Authorization', `Bearer ${token(owner)}`);
const index = await request(app).get('/api/agent-memory/editor-agent/machine-1')
.set('Authorization', `Bearer ${token(owner)}`);
const write = await request(app).patch(`${base}/long_term`)
.set('Authorization', `Bearer ${token(owner)}`)
.send({ content: 'Stolen edit', expectedUpdatedAt: new Date('2026-01-01').toISOString() });
const otherOwnerRead = await request(app).get(base)
.set('Authorization', `Bearer ${token(outsider)}`);
const adminRead = await request(app).get(base).set('Authorization', `Bearer ${token(admin)}`);
expect(read.status).toBe(403);
expect(index.status).toBe(403);
expect(write.status).toBe(403);
expect(otherOwnerRead.status).toBe(403);
expect(adminRead.status).toBe(200);
const memory = await AgentMemory.findOne({ agentName: 'editor-agent', instanceId: 'machine-1' }).lean();
expect(memory.sections.long_term.content).toBe('Old note');
expect(memory.content).toBe('Old note');
});
});
86 changes: 86 additions & 0 deletions backend/__tests__/service/agent-policy-settings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const express = require('express');
const request = require('supertest');
const jwt = require('jsonwebtoken');
const { setupMongoDb, closeMongoDb } = require('../utils/testUtils');
const User = require('../../models/User');
const Pod = require('../../models/Pod');
const AgentProfile = require('../../models/AgentProfile');
const { AgentInstallation } = require('../../models/AgentRegistry');
const agentConfig = require('../../routes/registry/agent-config');
const podAgents = require('../../routes/registry/pod-agents');

const SECRET = 'agent-policy-settings-test';
const token = (user) => jwt.sign({ id: String(user._id) }, SECRET);

describe('per-agent policy settings', () => {
let app; let owner; let outsider; let pod; let url;

beforeAll(async () => {
process.env.JWT_SECRET = SECRET;
await setupMongoDb();
app = express();
app.use(express.json());
app.use('/api/registry', agentConfig);
app.use('/api/registry', podAgents);
owner = await User.create({ username: 'policy-owner', email: 'policy-owner@test.com', password: 'password123' });
outsider = await User.create({ username: 'policy-outsider', email: 'policy-outsider@test.com', password: 'password123' });
pod = await Pod.create({ name: 'Policy pod', type: 'chat', createdBy: owner._id, members: [owner._id, outsider._id] });
url = `/api/registry/pods/${pod._id}/agents/policy-agent`;
});

afterAll(async () => closeMongoDb());

beforeEach(async () => {
await AgentInstallation.deleteMany({ agentName: 'policy-agent' });
await AgentProfile.deleteMany({ agentName: 'policy-agent' });
await AgentInstallation.create({ agentName: 'policy-agent', instanceId: 'machine-1',
podId: pod._id, version: '1', installedBy: owner._id,
config: new Map([['runtime', { runtimeType: 'wrapper', adapter: 'claude' }],
['environment', { adapter: 'claude', workspace: { path: '/repo' },
sandbox: { mode: 'read-only', network: { policy: 'restricted' } },
mcp: [{ name: 'safe', transport: 'stdio', command: ['node', 'mcp.js'] }] }]]) });
await AgentProfile.create({ agentId: 'policy-agent:machine-1', agentName: 'policy-agent',
instanceId: 'machine-1', podId: pod._id, name: 'Policy agent', purpose: 'Test', createdBy: owner._id });
});

it('writes approvals and context for the installer, then projects them', async () => {
const response = await request(app).patch(url).set('Authorization', `Bearer ${token(owner)}`)
.send({ instanceId: 'machine-1', toolPolicy: { allowed: ['commonly'], blocked: ['Bash'], requireApproval: ['Write'] },
contextPolicy: { maxTokens: 8000, compactionThreshold: 6000, summaryHours: 24,
includeMemory: true, includeSkills: false, includeSummaries: true } });
expect(response.status).toBe(200);
const profile = await AgentProfile.findOne({ agentName: 'policy-agent' }).lean();
expect(profile.toolPolicy.allowed).toEqual(['commonly']);
expect(profile.contextPolicy.includeSkills).toBe(false);
const listed = await request(app).get(`/api/registry/pods/${pod._id}/agents/policy-agent?instanceId=machine-1`)
.set('Authorization', `Bearer ${token(owner)}`);
expect(listed.status).toBe(200);
expect(listed.body.agent.profile.toolPolicy.requireApproval).toEqual(['Write']);
});

it('updates trust mode without dropping workspace, network or MCP declarations', async () => {
const response = await request(app).patch(url).set('Authorization', `Bearer ${token(owner)}`)
.send({ instanceId: 'machine-1', config: { environment: { sandbox: { mode: 'workspace' } } } });
expect(response.status).toBe(200);
const installation = await AgentInstallation.findOne({ agentName: 'policy-agent' });
const environment = installation.config.get('environment');
expect(environment.sandbox).toEqual({ mode: 'workspace', network: { policy: 'restricted' } });
expect(environment.workspace).toEqual({ path: '/repo' });
expect(environment.mcp).toHaveLength(1);
const listed = await request(app).get(`/api/registry/pods/${pod._id}/agents/policy-agent?instanceId=machine-1`)
.set('Authorization', `Bearer ${token(owner)}`);
expect(listed.body.agent.config.environment.sandbox).toEqual({ mode: 'workspace' });
expect(JSON.stringify(listed.body)).not.toContain('mcp.js');
});

it('refuses a pod member and invalid context before changing policy', async () => {
const denied = await request(app).patch(url).set('Authorization', `Bearer ${token(outsider)}`)
.send({ instanceId: 'machine-1', toolPolicy: { allowed: ['Bash'] } });
const invalid = await request(app).patch(url).set('Authorization', `Bearer ${token(owner)}`)
.send({ instanceId: 'machine-1', contextPolicy: { maxTokens: 100, compactionThreshold: 200 } });
expect(denied.status).toBe(403);
expect(invalid.status).toBe(400);
const profile = await AgentProfile.findOne({ agentName: 'policy-agent' }).lean();
expect(profile.toolPolicy.allowed).not.toContain('Bash');
});
});
Loading
Loading