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
29 changes: 28 additions & 1 deletion backend/__tests__/unit/routes/hosted.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading