From 49272c3cf322543049c46e45648e57d26cbabc3f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:11:05 -0700 Subject: [PATCH 1/3] feat(team): a never-connected agent's card shows the owner the command that connects it (TASK-716) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: Ember Agent-Model: claude/claude-opus-5-5/medium --- .../registry.pod-agents-fixcommand.test.js | 83 +++++++++++++++++++ backend/routes/registry/pod-agents.ts | 22 +++-- .../v2/__tests__/V2YourTeamSignal.test.tsx | 13 +++ frontend/src/v2/components/V2YourTeamPage.tsx | 4 + frontend/src/v2/v2.css | 10 +++ 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js diff --git a/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js b/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js new file mode 100644 index 0000000000..261fba9deb --- /dev/null +++ b/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js @@ -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.agentName === 'Nova' || 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(); + }); +}); diff --git a/backend/routes/registry/pod-agents.ts b/backend/routes/registry/pod-agents.ts index fac7ba324f..0e0e66fb27 100644 --- a/backend/routes/registry/pod-agents.ts +++ b/backend/routes/registry/pod-agents.ts @@ -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 `; 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 } : {}), }; }), }); diff --git a/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx b/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx index b4d7bd7d06..96f8988ccc 100644 --- a/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx +++ b/frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx @@ -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', diff --git a/frontend/src/v2/components/V2YourTeamPage.tsx b/frontend/src/v2/components/V2YourTeamPage.tsx index 3ced72bff1..8e1b257a55 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -53,6 +53,7 @@ interface AgentInstallationSummary { description?: string | null; installedBy?: string | null; connection?: AgentConnection; + fixCommand?: string; } interface TaskRow { @@ -432,6 +433,9 @@ const V2YourTeamPage: React.FC = () => { {(!a.connection || a.connection.state === 'connected') && state !== 'idle' ? ' ' : ''} {statusText} + {a.connection?.state === 'never' && a.fixCommand && ( + {a.fixCommand} + )} {where && ( Date: Sat, 31 Oct 2026 16:12:07 -0700 Subject: [PATCH 2/3] test(team): match the roster seat by its lowercased name (TASK-716) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: Ember Agent-Model: claude/claude-opus-5-5/medium --- .../unit/routes/registry.pod-agents-fixcommand.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js b/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js index 261fba9deb..eb896da462 100644 --- a/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js +++ b/backend/__tests__/unit/routes/registry.pod-agents-fixcommand.test.js @@ -41,7 +41,7 @@ const agentFor = async (viewerId, tokens = []) => { }); const res = await request(app).get(`/api/registry/pods/${podId}/agents`); expect(res.status).toBe(200); - return res.body.agents.find((a) => a.agentName === 'Nova' || a.name === 'Nova'); + return res.body.agents.find((a) => a.name === 'nova'); }; beforeAll(setupMongoDb); From c4be4d27e762bb6a4b8d7c83e309aecc9534a6f2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:17:48 -0700 Subject: [PATCH 3/3] fix(team): give the fix command its own class; .v2-team-card__command already exists (TASK-716) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: Ember Agent-Model: claude/claude-opus-5-5/medium --- frontend/src/v2/components/V2YourTeamPage.tsx | 2 +- frontend/src/v2/v2.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/v2/components/V2YourTeamPage.tsx b/frontend/src/v2/components/V2YourTeamPage.tsx index 8e1b257a55..850b1a0c73 100644 --- a/frontend/src/v2/components/V2YourTeamPage.tsx +++ b/frontend/src/v2/components/V2YourTeamPage.tsx @@ -434,7 +434,7 @@ const V2YourTeamPage: React.FC = () => { {statusText} {a.connection?.state === 'never' && a.fixCommand && ( - {a.fixCommand} + {a.fixCommand} )} {where && ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index acbe31733c..ccad5c1e53 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -2503,7 +2503,7 @@ body.modern-ui.v2-canvas { user-select: all; } -.v2-team-card__command { +.v2-team-card__fix { display: block; margin-top: 4px; font-family: var(--v2-font-mono, ui-monospace, monospace);