|
1 | 1 | import { describe, expect, it } from 'vitest'; |
2 | 2 | import { rigMetadataFixture } from '@/sync/__testdata__/rigMetadata'; |
3 | 3 | import type { Metadata } from '@/sync/storageTypes'; |
4 | | -import { messageSentSessionProperties } from './messageSentProperties'; |
| 4 | +import { messageSentClient, messageSentSessionProperties } from './messageSentProperties'; |
| 5 | + |
| 6 | +const props = ( |
| 7 | + metadata: Metadata | null | undefined, |
| 8 | + mode: Parameters<typeof messageSentSessionProperties>[2], |
| 9 | + machinePlatform?: string | null, |
| 10 | +) => messageSentSessionProperties('ios', metadata, mode, machinePlatform); |
| 11 | + |
| 12 | +const bot = { id: 'bot-1', name: 'Chief of Staff', username: 'chief', workspaceId: 'w-1', orderKey: '1' }; |
5 | 13 |
|
6 | 14 | describe('messageSentSessionProperties', () => { |
7 | 15 | it('reports Happy Agent version, model, provider kind and effort from the sent mode', () => { |
8 | | - expect(messageSentSessionProperties(rigMetadataFixture, { |
| 16 | + expect(messageSentSessionProperties('desktop', rigMetadataFixture, { |
9 | 17 | model: 'shared-model', |
10 | 18 | modelProviderId: 'claude', |
11 | 19 | effort: 'max', |
12 | | - })).toEqual({ |
| 20 | + }, null, (providerId) => `hash(${providerId})`)).toEqual({ |
| 21 | + client: 'desktop', |
| 22 | + target: 'session', |
| 23 | + bot_system_key: null, |
| 24 | + task_depth: null, |
13 | 25 | session_client: 'happy_agent', |
14 | 26 | happy_agent_version: '0.0.30', |
15 | 27 | model: 'shared-model', |
16 | 28 | model_provider_kind: 'claude', |
| 29 | + provider_account_hash: 'hash(claude)', |
17 | 30 | effort: 'max', |
| 31 | + agent_os: 'mac', |
18 | 32 | }); |
19 | 33 | }); |
20 | 34 |
|
21 | | - it('reports a custom provider by kind, not by its own name', () => { |
22 | | - const metadata: Metadata = { ...rigMetadataFixture, models: [], providers: [], provider: undefined }; |
23 | | - expect(messageSentSessionProperties(metadata, { model: 'm', modelProviderId: 'my_private_proxy' }).model_provider_kind) |
24 | | - .toBe('custom'); |
| 35 | + it('hashes the account the message went to, or the session provider, and only for Happy Agent', () => { |
| 36 | + const hash = (providerId: string) => `hash(${providerId})`; |
| 37 | + const send = (metadata: Metadata, modelProviderId?: string) => |
| 38 | + messageSentSessionProperties('ios', metadata, { modelProviderId }, null, hash).provider_account_hash; |
| 39 | + expect(send(rigMetadataFixture, 'claude_extra')).toBe('hash(claude_extra)'); |
| 40 | + expect(send(rigMetadataFixture)).toBe('hash(codex)'); |
| 41 | + expect(send({ ...rigMetadataFixture, provider: undefined })).toBeNull(); |
| 42 | + expect(send({ path: '/r', host: 'h', flavor: 'claude' } as Metadata, 'claude')).toBeNull(); |
| 43 | + expect(props(rigMetadataFixture, { modelProviderId: 'claude' }).provider_account_hash).toBeNull(); |
| 44 | + }); |
| 45 | + |
| 46 | + it('reports the task depth only when it is a whole number from zero up', () => { |
| 47 | + const depth = (value: unknown) => props({ ...rigMetadataFixture, depth: value } as Metadata, {}).task_depth; |
| 48 | + expect(depth(0)).toBe(0); |
| 49 | + expect(depth(2)).toBe(2); |
| 50 | + expect(depth(undefined)).toBeNull(); |
| 51 | + expect(depth(-1)).toBeNull(); |
| 52 | + expect(depth(1.5)).toBeNull(); |
| 53 | + expect(depth('2')).toBeNull(); |
| 54 | + }); |
| 55 | + |
| 56 | + it('reports the provider by its configured type as published, never by its account id', () => { |
| 57 | + const model = (providerId: string, providerKind: string) => ({ providerId, providerKind, id: 'm', code: 'm', value: 'M' }); |
| 58 | + const kind = (metadata: Metadata, providerId: string) => |
| 59 | + props(metadata, { model: 'm', modelProviderId: providerId }).model_provider_kind; |
| 60 | + const bare: Metadata = { ...rigMetadataFixture, models: [], providers: [], provider: undefined }; |
| 61 | + expect(kind({ ...bare, models: [model('my_bedrock', 'bedrock')] }, 'my_bedrock')).toBe('bedrock'); |
| 62 | + expect(kind({ ...bare, providers: [{ id: 'my_router', kind: 'openrouter', name: 'Mine' }] }, 'my_router')).toBe('openrouter'); |
| 63 | + // An older Happy Agent's own `custom` passes through unmapped. |
| 64 | + expect(kind({ ...bare, models: [model('my_proxy', 'custom')] }, 'my_proxy')).toBe('custom'); |
| 65 | + }); |
| 66 | + |
| 67 | + it('reports no provider kind rather than inventing one', () => { |
| 68 | + const bare: Metadata = { ...rigMetadataFixture, models: [], providers: [], provider: undefined }; |
| 69 | + expect(props(bare, { model: 'm', modelProviderId: 'unknown_account' }).model_provider_kind).toBeNull(); |
| 70 | + const kindless: Metadata = { ...bare, models: [{ providerId: 'p', id: 'm', code: 'm', value: 'M' }] }; |
| 71 | + expect(props(kindless, { model: 'm', modelProviderId: 'p' }).model_provider_kind).toBeNull(); |
25 | 72 | }); |
26 | 73 |
|
27 | 74 | it('marks CLI sessions and leaves Happy Agent fields empty', () => { |
28 | 75 | const metadata = { path: '/repo', host: 'mac', version: '1.2.5', flavor: 'codex' } as Metadata; |
29 | | - expect(messageSentSessionProperties(metadata, { model: 'gpt-5.6-sol', effort: 'high' })).toEqual({ |
| 76 | + expect(props(metadata, { model: 'gpt-5.6-sol', effort: 'high' })).toEqual({ |
| 77 | + client: 'ios', |
| 78 | + target: 'session', |
| 79 | + bot_system_key: null, |
| 80 | + task_depth: null, |
30 | 81 | session_client: 'cli', |
31 | 82 | happy_agent_version: null, |
32 | 83 | model: 'gpt-5.6-sol', |
33 | 84 | model_provider_kind: null, |
| 85 | + provider_account_hash: null, |
34 | 86 | effort: 'high', |
| 87 | + agent_os: null, |
35 | 88 | }); |
36 | 89 | }); |
37 | 90 |
|
| 91 | + it('reads the OS from the session, as Happy Agent and the CLI each write it', () => { |
| 92 | + const os = (value: string) => props({ path: '/r', host: 'h', os: value } as Metadata, {}).agent_os; |
| 93 | + expect(os('darwin 25.6.0')).toBe('mac'); |
| 94 | + expect(os('win32 10.0.26100')).toBe('win'); |
| 95 | + expect(os('linux')).toBe('linux'); |
| 96 | + expect(os('freebsd')).toBe('other'); |
| 97 | + }); |
| 98 | + |
| 99 | + it('falls back to the machine platform when the session has no OS', () => { |
| 100 | + const metadata = { path: '/r', host: 'h' } as Metadata; |
| 101 | + expect(props(metadata, {}, 'win32').agent_os).toBe('win'); |
| 102 | + expect(props({ ...metadata, os: 'linux 6.8' }, {}, 'darwin').agent_os).toBe('linux'); |
| 103 | + expect(props(metadata, {}, ' ').agent_os).toBeNull(); |
| 104 | + }); |
| 105 | + |
| 106 | + it('tells bots and the Chief of Staff apart only by the bot system key', () => { |
| 107 | + const target = (metadata: Metadata) => props(metadata, {}).target; |
| 108 | + expect(target({ ...rigMetadataFixture, bot })).toBeNull(); |
| 109 | + expect(target({ ...rigMetadataFixture, bot: { ...bot, systemKey: 'chief_of_staff' } })).toBe('chief_of_staff'); |
| 110 | + expect(target({ ...rigMetadataFixture, bot: { ...bot, systemKey: null } })).toBe('bot'); |
| 111 | + expect(target({ ...rigMetadataFixture, bot: { ...bot, systemKey: 'future_system_bot' } })).toBe('bot'); |
| 112 | + expect(props(null, undefined).target).toBeNull(); |
| 113 | + }); |
| 114 | + |
| 115 | + it('reports the raw system key of a system bot, never its name or id', () => { |
| 116 | + const key = (metadata: Metadata) => props(metadata, {}).bot_system_key; |
| 117 | + expect(key({ ...rigMetadataFixture, bot: { ...bot, systemKey: 'chief_of_staff' } })).toBe('chief_of_staff'); |
| 118 | + expect(key({ ...rigMetadataFixture, bot: { ...bot, systemKey: null } })).toBeNull(); |
| 119 | + expect(key({ ...rigMetadataFixture, bot })).toBeNull(); |
| 120 | + expect(key(rigMetadataFixture)).toBeNull(); |
| 121 | + }); |
| 122 | + |
38 | 123 | it('reports null model and effort when the agent default applies', () => { |
39 | 124 | const metadata = { path: '/repo', host: 'mac', flavor: 'claude' } as Metadata; |
40 | | - expect(messageSentSessionProperties(metadata, {})).toMatchObject({ model: null, effort: null }); |
41 | | - expect(messageSentSessionProperties(null, undefined).session_client).toBeNull(); |
| 125 | + expect(props(metadata, {})).toMatchObject({ model: null, effort: null }); |
| 126 | + expect(props(null, undefined).session_client).toBeNull(); |
| 127 | + }); |
| 128 | +}); |
| 129 | + |
| 130 | +describe('messageSentClient', () => { |
| 131 | + it('names the phone by its OS and the web build by where it runs', () => { |
| 132 | + expect(messageSentClient('ios', false)).toBe('ios'); |
| 133 | + expect(messageSentClient('android', false)).toBe('android'); |
| 134 | + expect(messageSentClient('web', false)).toBe('web'); |
| 135 | + expect(messageSentClient('web', true)).toBe('desktop'); |
42 | 136 | }); |
43 | 137 | }); |
0 commit comments