• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / toolhive-studio / 24661775587

20 Apr 2026 10:33AM UTC coverage: 65.836% (+3.9%) from 61.888%
24661775587

Pull #1938

github

peppescg
feat(ci): add direct fix fallback when TDD cannot reproduce bug

- Add Phase 2b: when Phase 1 writes a test but it passes (bug not
  reproduced), fall back to a direct fix without regression test
- Reduce Phase 1 internal retries from 5 to 3
- Increase Phase 2/2b max-turns from 80 to 150
- Rename job from "TDD Bug Fix" to "Bug Fix"
- Update skill docs with Phase 2b documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pull Request #1938: feat(ci): add TDD bug-fix agent and triage cron

3640 of 6028 branches covered (60.38%)

5756 of 8743 relevant lines covered (65.84%)

120.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.41
/main/src/cli/validation.ts
1
/**
2
 * CLI Alignment Validation
3
 * Every-launch validation logic for CLI alignment (THV-0020)
4
 */
5

6
import { app } from 'electron'
7
import * as Sentry from '@sentry/electron/main'
8
import { detectExternalCli, getCliInfo } from './cli-detection'
9
import { readMarkerFile, createMarkerForDesktopInstall } from './marker-file'
10
import {
11
  checkSymlink,
12
  createSymlink,
13
  getBundledCliPath,
14
  getMarkerTargetPath,
15
  isFlatpak,
16
  repairSymlink,
17
} from './symlink-manager'
18
import { configureShellPath, checkPathConfiguration } from './path-configurator'
19
import { getDesktopCliPath } from './constants'
20
import type { ValidationResult } from '@common/types/cli'
21
import type { CliAlignmentStatus, Platform } from './types'
22
import log from '../logger'
23

24
export async function validateCliAlignment(
25
  platform: Platform = process.platform as Platform
26
): Promise<ValidationResult> {
27
  return Sentry.startSpanManual(
28
    {
29
      name: 'CLI alignment validation',
6✔
30
      op: 'cli.validation',
31
      attributes: {
6✔
32
        'analytics.source': 'tracking',
33
        'analytics.type': 'event',
34
        'cli.platform': platform,
35
      },
36
    },
37
    async (span) => {
38
      log.info('Starting CLI alignment validation...')
39

40
      const external = await detectExternalCli(platform)
41
      if (external) {
42
        log.warn(`External CLI found at: ${external.path}`)
6✔
43
        span.setAttributes({
44
          'cli.status': 'external-cli-found',
6✔
45
          'cli.external_path': external.path,
6✔
46
          'cli.external_source': external.source,
1✔
47
        })
1✔
48
        span.end()
49
        return { status: 'external-cli-found', cli: external }
50
      }
51

52
      const marker = readMarkerFile()
1✔
53

1✔
54
      if (!marker) {
55
        log.info('No marker file found, treating as fresh install')
56
        span.setAttributes({ 'cli.status': 'fresh-install' })
5✔
57
        span.end()
58
        return { status: 'fresh-install' }
5✔
59
      }
1✔
60

1✔
61
      const symlink = checkSymlink(platform)
1✔
62

1✔
63
      if (!symlink.exists) {
64
        log.warn('CLI alignment issue: symlink-missing')
65
        span.setAttributes({ 'cli.status': 'symlink-missing' })
4✔
66
        span.end()
67
        return { status: 'symlink-missing' }
4✔
68
      }
1✔
69

1✔
70
      if (!symlink.targetExists) {
1✔
71
        log.warn('CLI alignment issue: symlink-broken')
1✔
72
        span.setAttributes({
73
          'cli.status': 'symlink-broken',
74
          'cli.symlink_target': symlink.target ?? 'unknown',
3✔
75
        })
1✔
76
        span.end()
1✔
77
        return { status: 'symlink-broken', target: symlink.target ?? 'unknown' }
78
      }
1!
79

80
      if (!symlink.isOurBinary) {
1✔
81
        log.warn('CLI alignment issue: symlink-tampered')
1!
82
        span.setAttributes({
83
          'cli.status': 'symlink-tampered',
84
          'cli.symlink_target': symlink.target ?? 'unknown',
2✔
85
        })
1✔
86
        span.end()
1✔
87
        return {
88
          status: 'symlink-tampered',
1!
89
          target: symlink.target ?? 'unknown',
90
        }
1✔
91
      }
1✔
92

93
      // Check and configure PATH if needed
1!
94
      const pathStatus = await checkPathConfiguration()
95
      if (!pathStatus.isConfigured) {
96
        log.info('PATH not configured, configuring now...')
97
        const pathResult = await configureShellPath()
98
        span.setAttribute('cli.path_configured', pathResult.success)
1✔
99
        if (!pathResult.success) {
100
          log.warn('Failed to configure PATH, user may need to add manually')
101
        }
1✔
102
      } else {
1!
103
        span.setAttribute('cli.path_configured', true)
×
104
      }
×
105

×
106
      log.info('CLI alignment validation passed')
×
107
      span.setAttributes({ 'cli.status': 'valid' })
×
108
      span.end()
109
      return { status: 'valid' }
110
    }
1✔
111
  )
112
}
113

1✔
114
/**
1✔
115
 * Handles validation results that can be auto-fixed without user interaction.
1✔
116
 * Returns the updated validation result after attempting auto-fixes.
1✔
117
 *
118
 * Cases handled automatically:
119
 * - valid: Updates marker file if needed
120
 * - fresh-install: Creates symlink and marker
121
 * - symlink-missing: Creates symlink and marker
122
 *
123
 * Cases requiring user interaction (returned as-is for renderer to handle):
124
 * - external-cli-found: User must uninstall external CLI
125
 * - symlink-broken: User must confirm repair
126
 * - symlink-tampered: User must confirm restore
127
 */
128
export async function handleValidationResult(
129
  result: ValidationResult,
130
  platform: Platform = process.platform as Platform
131
): Promise<ValidationResult> {
132
  return Sentry.startSpanManual(
133
    {
134
      name: 'CLI handle validation result',
135
      op: 'cli.handle_result',
136
      attributes: {
137
        'analytics.source': 'tracking',
10✔
138
        'analytics.type': 'event',
139
        'cli.input_status': result.status,
10✔
140
        'cli.platform': platform,
141
      },
142
    },
143
    async (span) => {
144
      switch (result.status) {
145
        case 'valid': {
146
          log.info('CLI alignment is valid')
147

148
          // Update marker file if desktop version changed (app was updated) or cli_version is unknown
149
          const marker = readMarkerFile()
150
          const currentDesktopVersion = app.getVersion()
151
          const needsUpdate =
10✔
152
            marker &&
153
            (marker.desktop_version !== currentDesktopVersion ||
4✔
154
              marker.cli_version === 'unknown')
155

156
          if (needsUpdate) {
4✔
157
            log.info(
4✔
158
              `Updating marker file (desktop: ${marker.desktop_version} -> ${currentDesktopVersion}, cli: ${marker.cli_version})...`
159
            )
4✔
160
            span.setAttributes({
161
              'cli.marker_updated': true,
162
              'cli.old_desktop_version': marker.desktop_version,
163
              'cli.new_desktop_version': currentDesktopVersion,
4✔
164
            })
3✔
165

166
            const cliPath = getDesktopCliPath(platform)
167

3✔
168
            // On Windows, we need to recopy the CLI since it's a copy not a symlink
169
            if (platform === 'win32') {
170
              log.info('Recopying CLI on Windows after app update...')
171
              const symlinkResult = createSymlink(platform)
172
              if (symlinkResult.success) {
173
                span.setAttribute('cli.windows_recopy', true)
3✔
174
                const cliInfo = await getCliInfo(cliPath)
175
                createMarkerForDesktopInstall({
176
                  cliVersion: cliInfo.version ?? 'unknown',
3✔
177
                  cliChecksum: symlinkResult.checksum,
2✔
178
                  platform,
2✔
179
                })
2✔
180
              } else {
1✔
181
                // Don't update marker on failure - next launch will retry
1✔
182
                log.error(
1✔
183
                  `Failed to recopy CLI on Windows: ${symlinkResult.error}`
1!
184
                )
185
                span.setAttributes({
186
                  'cli.windows_recopy': false,
187
                  'cli.windows_recopy_error': symlinkResult.error ?? 'unknown',
188
                })
189
              }
1✔
190
            } else {
191
              // macOS/Linux: symlink auto-updates, just update marker
192
              const cliInfo = await getCliInfo(cliPath)
1✔
193
              const targetPath = getMarkerTargetPath()
194
              createMarkerForDesktopInstall({
1!
195
                cliVersion: cliInfo.version ?? 'unknown',
196
                symlinkTarget: isFlatpak() ? undefined : targetPath,
197
                cliChecksum: marker.cli_checksum,
198
                platform,
199
                flatpakTarget: isFlatpak() ? targetPath : undefined,
1✔
200
              })
1✔
201
            }
1✔
202
          }
1!
203

1!
204
          span.setAttributes({ 'cli.output_status': 'valid' })
205
          span.end()
206
          return { status: 'valid' }
1!
207
        }
208

209
        // These cases require user interaction - return as-is for renderer to handle
210
        case 'external-cli-found':
211
          log.info('External CLI found - renderer will show issue page')
4✔
212
          span.setAttributes({
4✔
213
            'cli.output_status': 'external-cli-found',
4✔
214
            'cli.action_required': 'uninstall_external',
215
          })
216
          span.end()
217
          return result
218

1✔
219
        case 'symlink-broken':
1✔
220
          log.info('Symlink broken - renderer will show issue page')
221
          span.setAttributes({
222
            'cli.output_status': 'symlink-broken',
223
            'cli.action_required': 'repair_symlink',
1✔
224
          })
1✔
225
          span.end()
226
          return result
227

1✔
228
        case 'symlink-tampered':
1✔
229
          log.info('Symlink tampered - renderer will show issue page')
230
          span.setAttributes({
231
            'cli.output_status': 'symlink-tampered',
232
            'cli.action_required': 'restore_symlink',
1✔
233
          })
1✔
234
          span.end()
235
          return result
236

1✔
237
        // These cases can be auto-fixed without user interaction
1✔
238
        case 'symlink-missing':
239
        case 'fresh-install': {
240
          log.info('Performing fresh CLI installation...')
241

1✔
242
          const symlinkResult = createSymlink(platform)
1✔
243
          if (!symlinkResult.success) {
244
            log.error(`Failed to create CLI symlink: ${symlinkResult.error}`)
245
            span.setAttributes({
246
              'cli.output_status': 'error',
247
              'cli.error': symlinkResult.error ?? 'unknown',
3✔
248
              'cli.success': false,
249
            })
3✔
250
            span.end()
3✔
251
            // Return a special error status - the app can still run
1✔
252
            return result
1✔
253
          }
254

1!
255
          const cliPath = getDesktopCliPath(platform)
256
          const cliInfo = await getCliInfo(cliPath)
257
          const targetPath = getMarkerTargetPath()
1✔
258

259
          createMarkerForDesktopInstall({
1✔
260
            cliVersion: cliInfo.version ?? 'unknown',
261
            symlinkTarget:
262
              platform === 'win32' || isFlatpak() ? undefined : targetPath,
2✔
263
            cliChecksum: symlinkResult.checksum,
2✔
264
            flatpakTarget: isFlatpak() ? targetPath : undefined,
2✔
265
          })
266

2✔
267
          log.info(`CLI installed: version=${cliInfo.version}, path=${cliPath}`)
2!
268

269
          const pathResult = await configureShellPath()
7!
270
          if (!pathResult.success) {
271
            log.warn(
2!
272
              'Failed to configure shell PATH, user may need to add manually'
273
            )
274
          }
3✔
275

276
          log.info('Fresh CLI installation completed successfully')
3✔
277
          span.setAttributes({
2!
278
            'cli.output_status': 'valid',
×
279
            'cli.fresh_install': true,
280
            'cli.version': cliInfo.version ?? 'unknown',
281
            'cli.path': cliPath,
282
            'cli.path_configured': pathResult.success,
283
          })
2✔
284
          span.end()
2✔
285
          return { status: 'valid' }
286
        }
287
      }
2!
288
    }
289
  )
290
}
291

3✔
292
/**
3✔
293
 * Repairs a broken or tampered symlink.
294
 * Called from renderer via IPC when user confirms repair.
295
 */
296
export async function repairCliSymlink(
297
  platform: Platform = process.platform as Platform
298
): Promise<{ success: boolean; error?: string }> {
299
  return Sentry.startSpanManual(
300
    {
301
      name: 'CLI repair symlink',
302
      op: 'cli.repair',
303
      attributes: {
304
        'analytics.source': 'tracking',
2✔
305
        'analytics.type': 'event',
306
        'cli.platform': platform,
2✔
307
      },
308
    },
309
    async (span) => {
310
      log.info('Repairing CLI symlink...')
311

312
      const result = repairSymlink(platform)
313
      if (!result.success) {
314
        log.error(`Failed to repair symlink: ${result.error}`)
315
        span.setAttributes({
316
          'cli.success': false,
317
          'cli.error': result.error ?? 'unknown',
2✔
318
        })
319
        span.end()
2✔
320
        return result
2✔
321
      }
1✔
322

1✔
323
      // Update marker file after repair
324
      const cliPath = getDesktopCliPath(platform)
1!
325
      const cliInfo = await getCliInfo(cliPath)
326
      const targetPath = getMarkerTargetPath()
1✔
327
      createMarkerForDesktopInstall({
1✔
328
        cliVersion: cliInfo.version ?? 'unknown',
329
        symlinkTarget:
330
          platform === 'win32' || isFlatpak() ? undefined : targetPath,
331
        cliChecksum: result.checksum,
1✔
332
        flatpakTarget: isFlatpak() ? targetPath : undefined,
1✔
333
      })
1✔
334

1✔
335
      log.info('Symlink repaired successfully')
1!
336
      span.setAttributes({
337
        'cli.success': true,
4!
338
        'cli.version': cliInfo.version ?? 'unknown',
339
        'cli.path': cliPath,
1!
340
      })
341
      span.end()
342
      return { success: true }
2✔
343
    }
2✔
344
  )
345
}
2!
346

347
export async function getCliAlignmentStatus(
348
  platform: Platform = process.platform as Platform
2✔
349
): Promise<CliAlignmentStatus> {
2✔
350
  return Sentry.startSpanManual(
351
    {
352
      name: 'CLI get alignment status',
353
      op: 'cli.get_status',
354
      attributes: {
355
        'analytics.source': 'tracking',
2✔
356
        'analytics.type': 'event',
357
        'cli.platform': platform,
2✔
358
      },
359
    },
360
    async (span) => {
361
      const cliPath = getDesktopCliPath(platform)
362
      const marker = readMarkerFile()
363
      const symlink = checkSymlink(platform)
364
      const cliInfo = await getCliInfo(cliPath)
365

366
      const status = {
367
        isManaged: marker !== null && symlink.isOurBinary,
368
        cliPath,
2✔
369
        cliVersion: cliInfo.version,
2✔
370
        installMethod: marker?.install_method ?? null,
2✔
371
        symlinkTarget: symlink.target,
2✔
372
        isValid: symlink.exists && symlink.targetExists && symlink.isOurBinary,
373
        lastValidated: new Date().toISOString(),
2✔
374
      }
3✔
375

376
      span.setAttributes({
377
        'cli.is_managed': status.isManaged,
3✔
378
        'cli.is_valid': status.isValid,
379
        'cli.version': status.cliVersion ?? 'unknown',
4✔
380
        'cli.install_method': status.installMethod ?? 'none',
381
      })
382
      span.end()
383

2✔
384
      return status
385
    }
386
  )
3✔
387
}
3✔
388

389
export async function reinstallCliSymlink(
2✔
390
  platform: Platform = process.platform as Platform
391
): Promise<{ success: boolean; error?: string }> {
2✔
392
  return Sentry.startSpanManual(
393
    {
394
      name: 'CLI reinstall symlink',
395
      op: 'cli.reinstall',
396
      attributes: {
397
        'analytics.source': 'tracking',
×
398
        'analytics.type': 'event',
399
        'cli.platform': platform,
×
400
      },
401
    },
402
    async (span) => {
403
      const result = createSymlink(platform)
404

405
      if (result.success) {
406
        const bundledPath = getBundledCliPath()
407
        const cliInfo = await getCliInfo(bundledPath)
408
        const targetPath = getMarkerTargetPath()
409
        createMarkerForDesktopInstall({
410
          cliVersion: cliInfo.version ?? 'unknown',
×
411
          symlinkTarget:
412
            platform === 'win32' || isFlatpak() ? undefined : targetPath,
×
413
          cliChecksum: result.checksum,
×
414
          flatpakTarget: isFlatpak() ? targetPath : undefined,
×
415
        })
×
416
        span.setAttributes({
×
417
          'cli.success': true,
×
418
          'cli.version': cliInfo.version ?? 'unknown',
419
        })
×
420
      } else {
421
        span.setAttributes({
×
422
          'cli.success': false,
423
          'cli.error': result.error ?? 'unknown',
×
424
        })
425
      }
×
426

427
      span.end()
428
      return result
×
429
    }
430
  )
×
431
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc