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 })