/ src / components / auth / setup-wizard / utils.test.ts
utils.test.ts
  1  import assert from 'node:assert/strict'
  2  import { test } from 'node:test'
  3  import {
  4    stepIndex,
  5    defaultKitForPath,
  6    formatEndpointHost,
  7    getStarterKitsForPath,
  8    isLocalOpenClawEndpoint,
  9    resolveOpenClawDashboardUrl,
 10    getOpenClawErrorHint,
 11    requiresSetupProviderVerification,
 12    withHttpScheme,
 13    buildStarterDrafts,
 14    preferredConfiguredProvider,
 15  } from './utils'
 16  import type { ConfiguredProvider } from './types'
 17  
 18  // ---------------------------------------------------------------------------
 19  // stepIndex
 20  // ---------------------------------------------------------------------------
 21  
 22  test('stepIndex: profile → 0', () => {
 23    assert.equal(stepIndex('profile'), 0)
 24  })
 25  
 26  test('stepIndex: path → 1', () => {
 27    assert.equal(stepIndex('path'), 1)
 28  })
 29  
 30  test('stepIndex: providers → 2', () => {
 31    assert.equal(stepIndex('providers'), 2)
 32  })
 33  
 34  test('stepIndex: connect maps to providers index (2)', () => {
 35    assert.equal(stepIndex('connect'), 2)
 36  })
 37  
 38  test('stepIndex: agents → 3', () => {
 39    assert.equal(stepIndex('agents'), 3)
 40  })
 41  
 42  // ---------------------------------------------------------------------------
 43  // onboarding path defaults
 44  // ---------------------------------------------------------------------------
 45  
 46  test('defaultKitForPath returns personal assistant for quick and intent', () => {
 47    assert.equal(defaultKitForPath('quick'), 'personal_assistant')
 48    assert.equal(defaultKitForPath('intent'), 'personal_assistant')
 49  })
 50  
 51  test('defaultKitForPath returns blank workspace for manual', () => {
 52    assert.equal(defaultKitForPath('manual'), 'blank_workspace')
 53  })
 54  
 55  test('getStarterKitsForPath: quick exposes a reduced starter set', () => {
 56    const ids = getStarterKitsForPath('quick').map((kit) => kit.id)
 57    assert.deepEqual(ids, ['personal_assistant', 'research_copilot', 'builder_studio'])
 58  })
 59  
 60  test('getStarterKitsForPath: intent stays focused on broad starter shapes', () => {
 61    const ids = getStarterKitsForPath('intent').map((kit) => kit.id)
 62    assert.deepEqual(ids, [
 63      'personal_assistant',
 64      'research_copilot',
 65      'builder_studio',
 66      'operator_swarm',
 67      'inbox_triage',
 68      'data_analyst',
 69    ])
 70  })
 71  
 72  test('getStarterKitsForPath: manual keeps the full catalog', () => {
 73    const ids = new Set(getStarterKitsForPath('manual').map((kit) => kit.id))
 74    assert.equal(ids.has('blank_workspace'), true)
 75    assert.equal(ids.has('content_studio'), true)
 76    assert.equal(ids.has('openclaw_fleet'), true)
 77  })
 78  
 79  // ---------------------------------------------------------------------------
 80  // formatEndpointHost
 81  // ---------------------------------------------------------------------------
 82  
 83  test('formatEndpointHost extracts host:port', () => {
 84    assert.equal(formatEndpointHost('http://localhost:18789'), 'localhost:18789')
 85  })
 86  
 87  test('formatEndpointHost returns host without port when none specified', () => {
 88    assert.equal(formatEndpointHost('https://gateway.example.com'), 'gateway.example.com')
 89  })
 90  
 91  test('formatEndpointHost returns null for empty', () => {
 92    assert.equal(formatEndpointHost(''), null)
 93  })
 94  
 95  test('formatEndpointHost returns null for null', () => {
 96    assert.equal(formatEndpointHost(null), null)
 97  })
 98  
 99  test('formatEndpointHost adds scheme to bare host:port', () => {
100    assert.equal(formatEndpointHost('10.0.0.5:18789'), '10.0.0.5:18789')
101  })
102  
103  // ---------------------------------------------------------------------------
104  // isLocalOpenClawEndpoint
105  // ---------------------------------------------------------------------------
106  
107  test('isLocalOpenClawEndpoint: localhost → true', () => {
108    assert.equal(isLocalOpenClawEndpoint('http://localhost:18789'), true)
109  })
110  
111  test('isLocalOpenClawEndpoint: 127.0.0.1 → true', () => {
112    assert.equal(isLocalOpenClawEndpoint('http://127.0.0.1:18789'), true)
113  })
114  
115  test('isLocalOpenClawEndpoint: [::1] not matched (URL hostname includes brackets)', () => {
116    // URL parser returns hostname as "[::1]", which doesn't match the "::1" check
117    assert.equal(isLocalOpenClawEndpoint('http://[::1]:18789'), false)
118  })
119  
120  test('isLocalOpenClawEndpoint: remote → false', () => {
121    assert.equal(isLocalOpenClawEndpoint('http://gateway.example.com:18789'), false)
122  })
123  
124  test('isLocalOpenClawEndpoint: null → false', () => {
125    assert.equal(isLocalOpenClawEndpoint(null), false)
126  })
127  
128  test('isLocalOpenClawEndpoint: 0.0.0.0 → true', () => {
129    assert.equal(isLocalOpenClawEndpoint('http://0.0.0.0:18789'), true)
130  })
131  
132  // ---------------------------------------------------------------------------
133  // resolveOpenClawDashboardUrl
134  // ---------------------------------------------------------------------------
135  
136  test('resolveOpenClawDashboardUrl converts ws:// to http://', () => {
137    assert.equal(resolveOpenClawDashboardUrl('ws://localhost:18789/v1'), 'http://localhost:18789')
138  })
139  
140  test('resolveOpenClawDashboardUrl converts wss:// to https://', () => {
141    assert.equal(resolveOpenClawDashboardUrl('wss://gateway.example.com/path'), 'https://gateway.example.com')
142  })
143  
144  test('resolveOpenClawDashboardUrl strips path', () => {
145    assert.equal(resolveOpenClawDashboardUrl('http://localhost:18789/some/path'), 'http://localhost:18789')
146  })
147  
148  test('resolveOpenClawDashboardUrl defaults for null', () => {
149    assert.equal(resolveOpenClawDashboardUrl(null), 'http://localhost:18789')
150  })
151  
152  // ---------------------------------------------------------------------------
153  // getOpenClawErrorHint
154  // ---------------------------------------------------------------------------
155  
156  test('getOpenClawErrorHint: timeout', () => {
157    const hint = getOpenClawErrorHint('Connection timed out')
158    assert.ok(hint)
159    assert.ok(hint.includes('port'))
160  })
161  
162  test('getOpenClawErrorHint: 401', () => {
163    const hint = getOpenClawErrorHint('Returned 401 unauthorized')
164    assert.ok(hint)
165    assert.ok(hint.includes('token'))
166  })
167  
168  test('getOpenClawErrorHint: 405', () => {
169    const hint = getOpenClawErrorHint('405 Method Not Allowed')
170    assert.ok(hint)
171    assert.ok(hint.includes('chatCompletions'))
172  })
173  
174  test('getOpenClawErrorHint: econnrefused', () => {
175    const hint = getOpenClawErrorHint('connect ECONNREFUSED 127.0.0.1:18789')
176    assert.ok(hint)
177    assert.ok(hint.includes('running'))
178  })
179  
180  test('getOpenClawErrorHint: unrecognized error → null', () => {
181    assert.equal(getOpenClawErrorHint('something unknown happened'), null)
182  })
183  
184  // ---------------------------------------------------------------------------
185  // withHttpScheme
186  // ---------------------------------------------------------------------------
187  
188  test('withHttpScheme adds http:// to bare host', () => {
189    assert.equal(withHttpScheme('localhost:18789'), 'http://localhost:18789')
190  })
191  
192  test('withHttpScheme preserves existing http://', () => {
193    assert.equal(withHttpScheme('http://localhost:18789'), 'http://localhost:18789')
194  })
195  
196  test('withHttpScheme preserves existing https://', () => {
197    assert.equal(withHttpScheme('https://example.com'), 'https://example.com')
198  })
199  
200  test('withHttpScheme preserves ws://', () => {
201    assert.equal(withHttpScheme('ws://localhost:18789'), 'ws://localhost:18789')
202  })
203  
204  test('withHttpScheme preserves wss://', () => {
205    assert.equal(withHttpScheme('wss://example.com'), 'wss://example.com')
206  })
207  
208  // ---------------------------------------------------------------------------
209  // buildStarterDrafts — OpenClaw provider handling
210  // ---------------------------------------------------------------------------
211  
212  function makeConfiguredProvider(overrides: Partial<ConfiguredProvider> & { setupProvider: ConfiguredProvider['setupProvider']; provider?: ConfiguredProvider['provider'] }): ConfiguredProvider {
213    const { setupProvider, provider = setupProvider, ...rest } = overrides
214    return {
215      id: 'cp-1',
216      setupProvider,
217      provider,
218      name: 'Test Provider',
219      credentialId: null,
220      endpoint: null,
221      defaultModel: '',
222      gatewayProfileId: null,
223      verified: true,
224      ...rest,
225    }
226  }
227  
228  test('buildStarterDrafts assigns OpenClaw provider to drafts', () => {
229    const cp = makeConfiguredProvider({ setupProvider: 'openclaw', endpoint: 'http://localhost:18789' })
230    const drafts = buildStarterDrafts({
231      starterKitId: 'personal_assistant',
232      intentText: '',
233      configuredProviders: [cp],
234    })
235    assert.ok(drafts.length > 0, 'should produce at least one draft')
236    for (const d of drafts) {
237      assert.equal(d.provider, 'openclaw')
238      assert.equal(d.setupProvider, 'openclaw')
239      assert.equal(d.providerConfigId, cp.id)
240    }
241  })
242  
243  test('buildStarterDrafts OpenClaw drafts use empty model (not "default")', () => {
244    const cp = makeConfiguredProvider({ setupProvider: 'openclaw', defaultModel: '' })
245    const drafts = buildStarterDrafts({
246      starterKitId: 'personal_assistant',
247      intentText: '',
248      configuredProviders: [cp],
249    })
250    for (const d of drafts) {
251      // Model should be empty since the gateway controls the model
252      assert.equal(d.model, '')
253    }
254  })
255  
256  test('buildStarterDrafts OpenClaw drafts inherit endpoint from provider', () => {
257    const cp = makeConfiguredProvider({ setupProvider: 'openclaw', endpoint: 'http://10.0.0.5:18789' })
258    const drafts = buildStarterDrafts({
259      starterKitId: 'personal_assistant',
260      intentText: '',
261      configuredProviders: [cp],
262    })
263    for (const d of drafts) {
264      assert.equal(d.apiEndpoint, 'http://10.0.0.5:18789')
265    }
266  })
267  
268  test('buildStarterDrafts carries dashboardUrl through from ConfiguredProvider', () => {
269    const cp = makeConfiguredProvider({
270      setupProvider: 'openclaw',
271      endpoint: 'http://localhost:18789',
272      dashboardUrl: 'http://localhost:18789?token=my-secret',
273    })
274    // dashboardUrl lives on the ConfiguredProvider, not the draft — verify it's accessible
275    assert.equal(cp.dashboardUrl, 'http://localhost:18789?token=my-secret')
276  })
277  
278  test('preferredConfiguredProvider picks openclaw provider for openclaw template', () => {
279    const openclawCp = makeConfiguredProvider({ id: 'oc-1', setupProvider: 'openclaw' })
280    const openaiCp = makeConfiguredProvider({ id: 'oai-1', setupProvider: 'openai' })
281    const result = preferredConfiguredProvider(
282      { id: 'tmpl-1', name: 'Test', description: '', systemPrompt: '', tools: [], recommendedProviders: ['openclaw'] },
283      [openaiCp, openclawCp],
284    )
285    assert.equal(result?.id, 'oc-1')
286  })
287  
288  test('buildStarterDrafts carries custom runtime provider ids alongside custom setup provider state', () => {
289    const cp = makeConfiguredProvider({
290      setupProvider: 'custom',
291      provider: 'custom-openrouter',
292      defaultModel: 'openai/gpt-4.1',
293    })
294    const drafts = buildStarterDrafts({
295      starterKitId: 'personal_assistant',
296      intentText: '',
297      configuredProviders: [cp],
298    })
299  
300    for (const draft of drafts) {
301      assert.equal(draft.setupProvider, 'custom')
302      assert.equal(draft.provider, 'custom-openrouter')
303      assert.equal(draft.model, 'openai/gpt-4.1')
304    }
305  })
306  
307  test('buildStarterDrafts injects current intent into starter prompts', () => {
308    const cp = makeConfiguredProvider({
309      setupProvider: 'openai',
310      provider: 'openai',
311      defaultModel: 'gpt-4o',
312    })
313    const drafts = buildStarterDrafts({
314      starterKitId: 'personal_assistant',
315      intentText: 'Help me run weekly product research and turn it into follow-up tasks.',
316      configuredProviders: [cp],
317    })
318  
319    assert.match(drafts[0]?.systemPrompt || '', /Current user intent:/)
320    assert.match(drafts[0]?.systemPrompt || '', /weekly product research/i)
321  })
322  
323  test('buildStarterDrafts creates the delegate team starter pair', () => {
324    const cp = makeConfiguredProvider({
325      setupProvider: 'openai',
326      provider: 'openai',
327      defaultModel: 'gpt-4o',
328    })
329    const drafts = buildStarterDrafts({
330      starterKitId: 'operator_swarm',
331      intentText: '',
332      configuredProviders: [cp],
333    })
334  
335    assert.deepEqual(drafts.map((draft) => draft.name), ['Operator', 'Maker'])
336  })
337  
338  test('requiresSetupProviderVerification skips custom providers', () => {
339    assert.equal(requiresSetupProviderVerification('custom'), false)
340    assert.equal(requiresSetupProviderVerification('openclaw'), false)
341    assert.equal(requiresSetupProviderVerification('openai'), true)
342    assert.equal(requiresSetupProviderVerification('openrouter'), true)
343    assert.equal(requiresSetupProviderVerification('hermes'), true)
344  })