Skip to content

Commit 2ae9a1e

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@5e2cfb5098ad8551c77db0ba3e690acaea240079
1 parent f29f78b commit 2ae9a1e

7 files changed

Lines changed: 171 additions & 76 deletions

File tree

‎cli/src/utils/__tests__/osc-timeout-scenarios.test.ts‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, test, expect } from 'bun:test'
2+
import { withTimeoutOr } from '@codebuff/common/util/promise'
23

34
import {
4-
withTimeout,
55
getGlobalOscTimeout,
66
getQueryOscTimeout,
77
} from '../terminal-color-detection'
@@ -13,14 +13,14 @@ import {
1313

1414
describe('OSC Timeout Protection Scenarios', () => {
1515
describe('Scenario 1: Terminal ignores OSC queries entirely', () => {
16-
test('withTimeout returns fallback value when promise never resolves', async () => {
16+
test('withTimeoutOr returns fallback value when promise never resolves', async () => {
1717
// Simulate a terminal that ignores the OSC query completely
1818
const neverResolvingPromise = new Promise<string>(() => {
1919
// This promise never resolves, simulating a non-responsive terminal
2020
})
2121

2222
const startTime = Date.now()
23-
const result = await withTimeout(neverResolvingPromise, 100, 'fallback')
23+
const result = await withTimeoutOr(neverResolvingPromise, 100, 'fallback')
2424
const elapsed = Date.now() - startTime
2525

2626
expect(result).toBe('fallback')
@@ -31,27 +31,27 @@ describe('OSC Timeout Protection Scenarios', () => {
3131
test('returns null theme when detection hangs', async () => {
3232
const hangingDetection = new Promise<'dark' | 'light' | null>(() => {})
3333

34-
const result = await withTimeout(hangingDetection, 50, null)
34+
const result = await withTimeoutOr(hangingDetection, 50, null)
3535
expect(result).toBeNull()
3636
})
3737
})
3838

3939
describe('Scenario 2: Terminal responds very slowly', () => {
40-
test('withTimeout returns fallback for slow responses', async () => {
40+
test('withTimeoutOr returns fallback for slow responses', async () => {
4141
const slowPromise = new Promise<string>((resolve) => {
4242
setTimeout(() => resolve('slow-response'), 500)
4343
})
4444

45-
const result = await withTimeout(slowPromise, 100, 'timeout-fallback')
45+
const result = await withTimeoutOr(slowPromise, 100, 'timeout-fallback')
4646
expect(result).toBe('timeout-fallback')
4747
})
4848

49-
test('withTimeout returns actual value for responses within timeout', async () => {
49+
test('withTimeoutOr returns actual value for responses within timeout', async () => {
5050
const fastPromise = new Promise<string>((resolve) => {
5151
setTimeout(() => resolve('fast-response'), 10)
5252
})
5353

54-
const result = await withTimeout(fastPromise, 100, 'timeout-fallback')
54+
const result = await withTimeoutOr(fastPromise, 100, 'timeout-fallback')
5555
expect(result).toBe('fast-response')
5656
})
5757
})
@@ -71,7 +71,7 @@ describe('OSC Timeout Protection Scenarios', () => {
7171
test('multiple timeouts racing dont cause issues', async () => {
7272
// Create multiple promises that might complete at similar times
7373
const promises = Array.from({ length: 10 }, (_, i) =>
74-
withTimeout(
74+
withTimeoutOr(
7575
new Promise<number>((resolve) =>
7676
setTimeout(() => resolve(i), Math.random() * 100),
7777
),
@@ -89,17 +89,17 @@ describe('OSC Timeout Protection Scenarios', () => {
8989
})
9090

9191
test('cleanup happens even on timeout', async () => {
92-
// This test verifies that withTimeout properly cleans up its own timeout
92+
// This test verifies that withTimeoutOr properly cleans up its own timeout
9393
// even when the underlying promise doesn't resolve
9494
const neverResolves = new Promise<string>(() => {
9595
// This promise intentionally never resolves
9696
})
9797

9898
// This should timeout and return the fallback value
99-
const result = await withTimeout(neverResolves, 50, 'timeout')
99+
const result = await withTimeoutOr(neverResolves, 50, 'timeout')
100100
expect(result).toBe('timeout')
101101

102-
// Note: withTimeout cleans up its own setTimeout via the finally block
102+
// Note: withTimeoutOr cleans up its own setTimeout via the finally block
103103
// The underlying promise's resources would need to be cleaned up separately
104104
})
105105
})
@@ -125,34 +125,34 @@ describe('OSC Timeout Protection Scenarios', () => {
125125
})
126126

127127
describe('Scenario 6: Error handling', () => {
128-
test('rejected promises propagate through withTimeout', async () => {
128+
test('rejected promises propagate through withTimeoutOr', async () => {
129129
const rejectingPromise = Promise.reject(new Error('test error'))
130130

131131
await expect(
132-
withTimeout(rejectingPromise, 100, 'fallback'),
132+
withTimeoutOr(rejectingPromise, 100, 'fallback'),
133133
).rejects.toThrow('test error')
134134
})
135135

136136
test('timeout value can be any type', async () => {
137137
const hangingPromise = new Promise<{ theme: string }>(() => {})
138138

139-
const objectResult = await withTimeout(hangingPromise, 50, {
139+
const objectResult = await withTimeoutOr(hangingPromise, 50, {
140140
theme: 'default',
141141
})
142142
expect(objectResult).toEqual({ theme: 'default' })
143143

144144
const hangingPromise2 = new Promise<number[]>(() => {})
145-
const arrayResult = await withTimeout(hangingPromise2, 50, [1, 2, 3])
145+
const arrayResult = await withTimeoutOr(hangingPromise2, 50, [1, 2, 3])
146146
expect(arrayResult).toEqual([1, 2, 3])
147147
})
148148
})
149149

150150
describe('Scenario 7: Stacked timeouts (defense in depth)', () => {
151151
test('inner timeout fires before outer timeout', async () => {
152152
const innerPromise = new Promise<string>(() => {})
153-
const innerResult = withTimeout(innerPromise, 30, 'inner-timeout')
153+
const innerResult = withTimeoutOr(innerPromise, 30, 'inner-timeout')
154154

155-
const outerPromise = withTimeout(innerResult, 100, 'outer-timeout')
155+
const outerPromise = withTimeoutOr(innerResult, 100, 'outer-timeout')
156156

157157
const result = await outerPromise
158158
// Inner timeout should fire first
@@ -165,7 +165,7 @@ describe('OSC Timeout Protection Scenarios', () => {
165165
// This never resolves, simulating broken inner timeout
166166
})
167167

168-
const result = await withTimeout(brokenInnerPromise, 50, 'outer-saved-us')
168+
const result = await withTimeoutOr(brokenInnerPromise, 50, 'outer-saved-us')
169169
expect(result).toBe('outer-saved-us')
170170
})
171171
})

‎cli/src/utils/__tests__/terminal-color-detection.test.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2+
import { withTimeoutOr } from '@codebuff/common/util/promise'
23

34
import {
45
parseOSCResponse,
56
calculateBrightness,
67
themeFromBgColor,
78
themeFromFgColor,
89
terminalSupportsOSC,
9-
withTimeout,
1010
getGlobalOscTimeout,
1111
getQueryOscTimeout,
1212
} from '../terminal-color-detection'
@@ -196,48 +196,48 @@ describe('themeFromFgColor', () => {
196196
})
197197

198198
// ============================================================================
199-
// withTimeout Tests
199+
// withTimeoutOr Tests
200200
// ============================================================================
201201

202-
describe('withTimeout', () => {
202+
describe('withTimeoutOr', () => {
203203
test('returns promise result if it resolves before timeout', async () => {
204204
const fastPromise = Promise.resolve('success')
205-
const result = await withTimeout(fastPromise, 1000, 'timeout')
205+
const result = await withTimeoutOr(fastPromise, 1000, 'timeout')
206206
expect(result).toBe('success')
207207
})
208208

209209
test('returns timeout value if promise takes too long', async () => {
210210
const slowPromise = new Promise<string>((resolve) => {
211211
setTimeout(() => resolve('late'), 500)
212212
})
213-
const result = await withTimeout(slowPromise, 50, 'timeout')
213+
const result = await withTimeoutOr(slowPromise, 50, 'timeout')
214214
expect(result).toBe('timeout')
215215
})
216216

217217
test('returns null timeout value', async () => {
218218
const slowPromise = new Promise<string | null>((resolve) => {
219219
setTimeout(() => resolve('late'), 500)
220220
})
221-
const result = await withTimeout(slowPromise, 50, null)
221+
const result = await withTimeoutOr(slowPromise, 50, null)
222222
expect(result).toBeNull()
223223
})
224224

225225
test('clears timeout after promise resolves', async () => {
226226
const fastPromise = Promise.resolve('success')
227227
// This should not cause any issues with dangling timeouts
228-
await withTimeout(fastPromise, 10000, 'timeout')
228+
await withTimeoutOr(fastPromise, 10000, 'timeout')
229229
// If the timeout wasn't cleared, this test would hang
230230
})
231231

232232
test('handles rejected promises', async () => {
233233
const failingPromise = Promise.reject(new Error('test error'))
234-
await expect(withTimeout(failingPromise, 1000, 'timeout')).rejects.toThrow(
234+
await expect(withTimeoutOr(failingPromise, 1000, 'timeout')).rejects.toThrow(
235235
'test error',
236236
)
237237
})
238238

239239
test('handles immediate resolution', async () => {
240-
const result = await withTimeout(Promise.resolve(42), 0, -1)
240+
const result = await withTimeoutOr(Promise.resolve(42), 0, -1)
241241
// Promise.resolve is always faster than setTimeout(0)
242242
expect(result).toBe(42)
243243
})

‎cli/src/utils/auth-token-store.ts‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*/
2323

2424
import { getCliEnv } from './env'
25+
import { withTimeout } from '@codebuff/common/util/promise'
2526

2627
export type SecretStore = {
2728
get(): Promise<string | null>
@@ -31,21 +32,7 @@ export type SecretStore = {
3132

3233
const KEYCHAIN_TIMEOUT_MS = 2_000
3334

34-
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
35-
return new Promise((resolve, reject) => {
36-
const timer = setTimeout(() => reject(new Error('keychain timed out')), ms)
37-
promise.then(
38-
(value) => {
39-
clearTimeout(timer)
40-
resolve(value)
41-
},
42-
(error) => {
43-
clearTimeout(timer)
44-
reject(error)
45-
},
46-
)
47-
})
48-
}
35+
const KEYCHAIN_TIMEOUT_MESSAGE = 'keychain timed out'
4936

5037
type BunSecrets = {
5138
get(options: { service: string; name: string }): Promise<string | null>
@@ -63,11 +50,24 @@ export function bunSecretStore(
6350
?.secrets
6451
if (!secrets) return null
6552
return {
66-
get: () => withTimeout(secrets.get({ service, name }), KEYCHAIN_TIMEOUT_MS),
53+
get: () =>
54+
withTimeout(
55+
secrets.get({ service, name }),
56+
KEYCHAIN_TIMEOUT_MS,
57+
KEYCHAIN_TIMEOUT_MESSAGE,
58+
),
6759
set: (value) =>
68-
withTimeout(secrets.set({ service, name, value }), KEYCHAIN_TIMEOUT_MS),
60+
withTimeout(
61+
secrets.set({ service, name, value }),
62+
KEYCHAIN_TIMEOUT_MS,
63+
KEYCHAIN_TIMEOUT_MESSAGE,
64+
),
6965
delete: async () => {
70-
await withTimeout(secrets.delete({ service, name }), KEYCHAIN_TIMEOUT_MS)
66+
await withTimeout(
67+
secrets.delete({ service, name }),
68+
KEYCHAIN_TIMEOUT_MS,
69+
KEYCHAIN_TIMEOUT_MESSAGE,
70+
)
7171
},
7272
}
7373
}

‎cli/src/utils/exit-cleanly.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { withTimeoutOr } from '@codebuff/common/util/promise'
12
import { flushAdEngagementOnExit } from '../ads/use-ad-engagement'
23
import { flushAnalytics } from './analytics'
34
import { IS_FREEBUFF } from './constants'
45
import { stopEngagementTracking } from './engagement'
56
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
67
import { drainClientLogs } from './log-shipper'
78
import { settleInterruptedSponsoredRun } from './sponsored-run-exit'
8-
import { withTimeout } from './terminal-color-detection'
99

1010
const EXIT_CLEANUP_TIMEOUT_MS = 1_000
1111

@@ -134,7 +134,7 @@ export const exitCliCleanly = createExitCliCleanly({
134134
}
135135
},
136136
waitForRemoteCleanup: async (tasks) => {
137-
await withTimeout(
137+
await withTimeoutOr(
138138
Promise.allSettled(tasks),
139139
EXIT_CLEANUP_TIMEOUT_MS,
140140
undefined,

‎cli/src/utils/terminal-color-detection.ts‎

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,14 @@
1212
import { openSync, closeSync, writeSync, constants } from 'fs'
1313

1414
import { getCliEnv } from './env'
15+
import { withTimeoutOr } from '@codebuff/common/util/promise'
1516

1617
import type { CliEnv } from '../types/env'
1718

1819
// Timeout constants
1920
const OSC_QUERY_TIMEOUT_MS = 500 // Timeout for individual OSC query
2021
const GLOBAL_OSC_TIMEOUT_MS = 2000 // Global timeout for entire detection process
2122

22-
/**
23-
* Wrap a promise with a timeout
24-
* @param promise - The promise to wrap
25-
* @param timeoutMs - Timeout in milliseconds
26-
* @param timeoutValue - Value to return on timeout
27-
*/
28-
export function withTimeout<T>(
29-
promise: Promise<T>,
30-
timeoutMs: number,
31-
timeoutValue: T,
32-
): Promise<T> {
33-
let timeoutId: NodeJS.Timeout | null = null
34-
35-
const timeoutPromise = new Promise<T>((resolve) => {
36-
timeoutId = setTimeout(() => {
37-
resolve(timeoutValue)
38-
}, timeoutMs)
39-
})
40-
41-
return Promise.race([promise, timeoutPromise]).finally(() => {
42-
if (timeoutId) {
43-
clearTimeout(timeoutId)
44-
}
45-
})
46-
}
47-
4823
/**
4924
* Check if the current terminal supports OSC color queries
5025
*/
@@ -449,7 +424,7 @@ async function detectTerminalThemeCore(
449424
*/
450425
export async function detectTerminalTheme(): Promise<'dark' | 'light' | null> {
451426
try {
452-
return await withTimeout(
427+
return await withTimeoutOr(
453428
detectTerminalThemeCore(),
454429
GLOBAL_OSC_TIMEOUT_MS,
455430
null,

0 commit comments

Comments
 (0)