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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// TASK-716: a never-connected BYO seat carries the command that makes it run,
// for the installer only, and only while it has never polled.
const request = require('supertest');
const express = require('express');
const mongoose = require('mongoose');

let mockViewer = '6a0000000000000000000002';
jest.mock('../../../middleware/auth', () => (req, res, next) => {
req.userId = mockViewer;
req.user = { id: mockViewer };
next();
});
jest.mock('../../../services/dmService', () => ({ canViewPod: jest.fn().mockResolvedValue(true) }));

const { setupMongoDb, closeMongoDb, clearMongoDb } = require('../../utils/testUtils');
const { AgentInstallation } = require('../../../models/AgentRegistry');
const Pod = require('../../../models/Pod');
const AgentIdentityService = require('../../../services/agentIdentityService');
const podAgentsRouter = require('../../../routes/registry/pod-agents');

const app = express();
app.use(express.json());
app.use('/api/registry', podAgentsRouter);

const owner = new mongoose.Types.ObjectId('6a0000000000000000000002');
const other = new mongoose.Types.ObjectId('6a0000000000000000000003');
const podId = new mongoose.Types.ObjectId();
const username = AgentIdentityService.buildAgentUsername('Nova', 'default');

const agentFor = async (viewerId, tokens = []) => {
mockViewer = String(viewerId);
await AgentInstallation.create({
agentName: 'Nova',
instanceId: 'default',
podId,
version: '1.0.0',
installedBy: owner,
status: 'active',
config: { runtime: { runtimeType: 'wrapper', host: 'byo' } },
runtimeTokens: tokens,
});
const res = await request(app).get(`/api/registry/pods/${podId}/agents`);
expect(res.status).toBe(200);
return res.body.agents.find((a) => a.name === 'nova');
};

beforeAll(setupMongoDb);
afterAll(closeMongoDb);
beforeEach(async () => {
await Pod.collection.insertOne({
_id: podId, name: 'p', type: 'chat', createdBy: owner, members: [owner, other],
});
await mongoose.connection.collection('users').insertOne({
username,
email: `${username}@agents.commonly.invalid`,
isBot: true,
botMetadata: { agentName: 'Nova', instanceId: 'default' },
});
});
afterEach(async () => {
await clearMongoDb();
await mongoose.connection.collection('users').deleteMany({});
});

describe('never-connected seat command (TASK-716)', () => {
it('gives the installer the run command while the seat has never polled', async () => {
const agent = await agentFor(owner);
expect(agent.connection.state).toBe('never');
expect(agent.fixCommand).toBe('commonly agent run nova');
});

it('gives another member no command', async () => {
const agent = await agentFor(other);
expect(agent.connection.state).toBe('never');
expect(agent.fixCommand).toBeUndefined();
});

it('drops the command once the seat has polled', async () => {
const agent = await agentFor(owner, [{ tokenHash: 'h', lastUsedAt: new Date() }]);
expect(agent.connection.state).not.toBe('never');
expect(agent.fixCommand).toBeUndefined();
});
});
22 changes: 15 additions & 7 deletions backend/routes/registry/pod-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,15 +405,23 @@ podAgentsRouter.get('/pods/:podId/agents', auth, async (req: any, res: any) => {
|| (runtimeType === 'webhook' && !payload?.runtime?.host);
if (!connectionAvailable || !user || !hasRuntimeConnection
|| normalizeConfigMap(i.config)?.runtime?.webhookUrl) return seatPayload;
const connection = deriveAgentConnection({
runtimeType: daemonBound ? 'wrapper' : runtimeType,
credentials: credentialByAgent.get(String(user._id)) || [],
machineCredentials: credentialByMachine.get(String(machineId)) || [],
embeddedTokens: [...(user.agentRuntimeTokens || []), ...(i.runtimeTokens || [])],
machineLastSeenAt: machineById.get(String(machineId))?.lastSeenAt || null,
});
// TASK-716: the command that makes a never-connected seat run goes to
// the installer only, the same rule /agent-states applies to fixCommand.
// It is `commonly agent run <name>`; no token or URL is in it.
// eslint-disable-next-line global-require
const fixCommand = connection?.state === 'never' && installedByViewer(i)
? require('../../services/agentStateService').runInstructionFor(i) : null;
return {
...seatPayload,
connection: deriveAgentConnection({
runtimeType: daemonBound ? 'wrapper' : runtimeType,
credentials: credentialByAgent.get(String(user._id)) || [],
machineCredentials: credentialByMachine.get(String(machineId)) || [],
embeddedTokens: [...(user.agentRuntimeTokens || []), ...(i.runtimeTokens || [])],
machineLastSeenAt: machineById.get(String(machineId))?.lastSeenAt || null,
}),
connection,
...(fixCommand ? { fixCommand } : {}),
};
}),
});
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ describe('Your Team (direction C)', () => {
expect(within(cardOf('Local')).getByRole('button', { name: 'Talk to Local one-to-one' })).toBeInTheDocument();
});

test('a never-connected card shows the run command when the server sends one, and not otherwise', async () => {
renderPage({ p1Agents: [
{ name: 'local', instanceId: 'default', displayName: 'Local', userId: 'local-user', installedBy: 'u1',
runtime: { runtimeType: 'webhook' }, connection: { state: 'never', lastSeenAt: null },
fixCommand: 'commonly agent run local' },
{ name: 'mate', instanceId: 'default', displayName: 'Mate', userId: 'mate-user', installedBy: 'u2',
runtime: { runtimeType: 'webhook' }, connection: { state: 'never', lastSeenAt: null } },
] });
await waitFor(() => expect(screen.getByText('Local')).toBeInTheDocument());
expect(within(cardOf('Local')).getByTestId('team-card-fix-command')).toHaveTextContent('commonly agent run local');
expect(within(cardOf('Mate')).queryByTestId('team-card-fix-command')).toBeNull();
});

test('a stale BYO listener shows elapsed minutes and Reconnect', async () => {
renderPage({ p1Agents: [{
name: 'local', instanceId: 'default', displayName: 'Local', userId: 'local-user',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/v2/components/V2YourTeamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface AgentInstallationSummary {
description?: string | null;
installedBy?: string | null;
connection?: AgentConnection;
fixCommand?: string;
}

interface TaskRow {
Expand Down Expand Up @@ -432,6 +433,9 @@ const V2YourTeamPage: React.FC = () => {
{(!a.connection || a.connection.state === 'connected') && state !== 'idle' ? ' ' : ''}
{statusText}
</div>
{a.connection?.state === 'never' && a.fixCommand && (
<code className="v2-team-card__fix" data-testid="team-card-fix-command">{a.fixCommand}</code>
)}
</div>
{where && (
<span
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,16 @@ body.modern-ui.v2-canvas {
user-select: all;
}

.v2-team-card__fix {
display: block;
margin-top: 4px;
font-family: var(--v2-font-mono, ui-monospace, monospace);
font-size: 12px;
color: var(--v2-text-secondary);
overflow-wrap: anywhere;
user-select: all;
}

.v2-chat__agent-room-status--wait {
border-color: var(--v2-border);
background: var(--v2-surface-hover);
Expand Down
Loading