diff --git a/backend/__tests__/unit/routes/hosted.test.js b/backend/__tests__/unit/routes/hosted.test.js index 1bc46fadff..21c4908049 100644 --- a/backend/__tests__/unit/routes/hosted.test.js +++ b/backend/__tests__/unit/routes/hosted.test.js @@ -69,7 +69,10 @@ jest.mock('../../../services/nativeRuntimeService', () => ({ const mockMarkByoOpened = jest.fn(); jest.mock('../../../services/onboardingMarks', () => ({ markByoOpened: (...args) => mockMarkByoOpened(...args) })); const mockCompleteConnectAgent = jest.fn(); +// Spread the real module so only the writer is replaced (Sprint Review on +// #2252: a factory that lists exports silently drops the rest). jest.mock('../../../services/starterTaskService', () => ({ + ...jest.requireActual('../../../services/starterTaskService'), completeConnectAgentStarterTask: (...args) => mockCompleteConnectAgent(...args), })); jest.mock('../../../routes/registry/tokens', () => ({ @@ -486,7 +489,31 @@ describe('/api/hosted on the native engine', () => { expect(mockCompleteConnectAgent).toHaveBeenCalledTimes(1); const [{ podIds, agentLabel }] = mockCompleteConnectAgent.mock.calls[0]; expect(podIds.map(String)).toEqual(['pod-1']); - expect(agentLabel).toBeTruthy(); + // The label reaches the person in the card's update line, so pin the + // value, not truthiness (Sprint Review on #2252: a constant passed that). + expect(agentLabel).toBe('scout'); + }); + + it('names the agent on the card by its display name first', async () => { + // Its own rate-limit bucket: the limiter keys on Authorization (auth is + // mocked), and this file's other requests already sit at its 30-a-minute + // ceiling, so two more from the shared key 429 a later test. + const ownBucket = { Authorization: 'Bearer hosted-label-test' }; + mockUserFindOne.mockResolvedValue({ + _id: 'bot-1', username: 'scout', agentRuntimeTokens: [], botMetadata: { displayName: 'Research Scout' }, + }); + mockFindOne.mockResolvedValue(nativeInstallation()); + await request(app).post('/api/hosted/provision').set(ownBucket).send({ agentName: 'scout' }); + expect(mockCompleteConnectAgent.mock.calls[0][0].agentLabel).toBe('Research Scout'); + + mockCompleteConnectAgent.mockClear(); + mockUserFindOne.mockResolvedValue({ _id: 'bot-1', username: 'scout', agentRuntimeTokens: [] }); + mockFindOne.mockResolvedValue(makeInstallation({ + displayName: 'Lab Notes', + config: new Map([['runtime', { runtimeType: 'hosted' }], ['hosted', { engine: 'native', provisionedAt: PROVISIONED }]]), + })); + await request(app).post('/api/hosted/provision').set(ownBucket).send({ agentName: 'scout' }); + expect(mockCompleteConnectAgent.mock.calls[0][0].agentLabel).toBe('Lab Notes'); }); it('provision of a native install 503s when the native runtime is unavailable, even with the worker up', async () => { diff --git a/backend/__tests__/unit/services/nativeRuntimeService.toolGating.test.js b/backend/__tests__/unit/services/nativeRuntimeService.toolGating.test.js index 252ae3871c..dc308fee32 100644 --- a/backend/__tests__/unit/services/nativeRuntimeService.toolGating.test.js +++ b/backend/__tests__/unit/services/nativeRuntimeService.toolGating.test.js @@ -93,6 +93,22 @@ describe('toolsForConfig', () => { expect(offer.function.description).toMatch(/no invitation code/); }); + // Sprint Review on #2250: the arm above compares the description with + // hostedCaps(), the source it is built from, so a hardcoded literal matching + // today's caps passes it too. Loaded under other caps, only a description + // derived from the cap source follows. + test('the hosted offer follows the cap source, so it cannot be a copied literal', () => { + const caps = { HOSTED_TURNS_PER_DAY: '7', HOSTED_TURNS_PER_DAY_PRO: '70', HOSTED_AGENTS_PER_USER: '2' }; + withEnv(caps, () => { + jest.isolateModules(() => { + // eslint-disable-next-line global-require + const { TOOLS: reloaded } = require('../../../services/nativeRuntimeService'); + const offer = reloaded.find((t) => t.function.name === 'commonly_offer_hosted_agent'); + expect(offer.function.description).toContain('2 hosted agents per account and 7 turns a day (70 on Pro)'); + }); + }); + }); + test('agent_status is declaration-gated like every other tool', () => { // Scout declares it; a legacy config or a minimal allowlist does not // receive it implicitly.