Skip to content

Commit da6ab1d

Browse files
committed
feat(agent-runtime): Handle PromptResult abort semantics in core functions
1 parent 2c84446 commit da6ab1d

File tree

11 files changed

+569
-37
lines changed

11 files changed

+569
-37
lines changed

cli/src/hooks/use-fingerprint.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import { calculateFingerprint, generateFingerprintIdSync } from '../utils/fingerprint'
4+
import { logger } from '../utils/logger'
5+
6+
interface UseFingerprintResult {
7+
fingerprintId: string
8+
isEnhanced: boolean
9+
isLoading: boolean
10+
}
11+
12+
/**
13+
* React hook for generating a hardware-based fingerprint.
14+
*
15+
* Immediately provides a legacy fingerprint for responsiveness,
16+
* then asynchronously generates an enhanced fingerprint if possible.
17+
*
18+
* The fingerprint is stable across re-renders (generated once on mount).
19+
*/
20+
export function useFingerprint(): UseFingerprintResult {
21+
// Start with a sync legacy fingerprint for immediate availability
22+
const [state, setState] = useState<UseFingerprintResult>(() => ({
23+
fingerprintId: generateFingerprintIdSync(),
24+
isEnhanced: false,
25+
isLoading: true,
26+
}))
27+
28+
useEffect(() => {
29+
let cancelled = false
30+
31+
const generateEnhanced = async () => {
32+
try {
33+
const enhancedFingerprint = await calculateFingerprint()
34+
if (!cancelled) {
35+
setState({
36+
fingerprintId: enhancedFingerprint,
37+
isEnhanced: enhancedFingerprint.startsWith('enhanced-'),
38+
isLoading: false,
39+
})
40+
}
41+
} catch (error) {
42+
logger.error(error, 'Failed to generate enhanced fingerprint')
43+
if (!cancelled) {
44+
// Keep the legacy fingerprint we already have
45+
setState((prev) => ({
46+
...prev,
47+
isLoading: false,
48+
}))
49+
}
50+
}
51+
}
52+
53+
generateEnhanced()
54+
55+
return () => {
56+
cancelled = true
57+
}
58+
}, [])
59+
60+
return state
61+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import { getFingerprintType, generateFingerprintIdSync } from '../fingerprint'
4+
5+
describe('fingerprint utilities', () => {
6+
describe('getFingerprintType', () => {
7+
describe('enhanced fingerprints', () => {
8+
test('should detect enhanced- prefix as enhanced_cli', () => {
9+
expect(getFingerprintType('enhanced-abc123')).toBe('enhanced_cli')
10+
})
11+
12+
test('should detect enhanced fingerprint with full hash', () => {
13+
const fullHash = 'enhanced-Ks7mN2pQxR3vW5yZ8aB4cD6eF9gH1iJ2kL4mN5oP7qR8sT0uV1wX3yZ'
14+
expect(getFingerprintType(fullHash)).toBe('enhanced_cli')
15+
})
16+
17+
test('should detect enhanced- prefix with empty suffix', () => {
18+
expect(getFingerprintType('enhanced-')).toBe('enhanced_cli')
19+
})
20+
})
21+
22+
describe('legacy fingerprints', () => {
23+
test('should detect codebuff-cli- prefix as legacy', () => {
24+
expect(getFingerprintType('codebuff-cli-abc12345')).toBe('legacy')
25+
})
26+
27+
test('should detect legacy- prefix as legacy', () => {
28+
expect(getFingerprintType('legacy-abc123-xyz789')).toBe('legacy')
29+
})
30+
31+
test('should detect codebuff-cli- prefix with any suffix', () => {
32+
expect(getFingerprintType('codebuff-cli-')).toBe('legacy')
33+
expect(getFingerprintType('codebuff-cli-randomsuffix')).toBe('legacy')
34+
expect(getFingerprintType('codebuff-cli-12345678')).toBe('legacy')
35+
})
36+
37+
test('should detect legacy- prefix with any suffix', () => {
38+
expect(getFingerprintType('legacy-')).toBe('legacy')
39+
expect(getFingerprintType('legacy-hash-suffix')).toBe('legacy')
40+
})
41+
})
42+
43+
describe('unknown fingerprints', () => {
44+
test('should return unknown for empty string', () => {
45+
expect(getFingerprintType('')).toBe('unknown')
46+
})
47+
48+
test('should return unknown for unrecognized prefix', () => {
49+
expect(getFingerprintType('unknown-prefix-123')).toBe('unknown')
50+
})
51+
52+
test('should return unknown for partial matches', () => {
53+
// Should not match if prefix is incomplete
54+
expect(getFingerprintType('enhance-abc123')).toBe('unknown')
55+
expect(getFingerprintType('codebuff-abc123')).toBe('unknown')
56+
expect(getFingerprintType('lega-abc123')).toBe('unknown')
57+
})
58+
59+
test('should return unknown for SDK fingerprints', () => {
60+
expect(getFingerprintType('codebuff-sdk-abc123')).toBe('unknown')
61+
})
62+
63+
test('should return unknown for random strings', () => {
64+
expect(getFingerprintType('random-string')).toBe('unknown')
65+
expect(getFingerprintType('abc123')).toBe('unknown')
66+
expect(getFingerprintType('fingerprint')).toBe('unknown')
67+
})
68+
69+
test('should be case-sensitive', () => {
70+
expect(getFingerprintType('Enhanced-abc123')).toBe('unknown')
71+
expect(getFingerprintType('ENHANCED-abc123')).toBe('unknown')
72+
expect(getFingerprintType('Codebuff-cli-abc123')).toBe('unknown')
73+
expect(getFingerprintType('LEGACY-abc123')).toBe('unknown')
74+
})
75+
})
76+
})
77+
78+
describe('generateFingerprintIdSync', () => {
79+
describe('format validation', () => {
80+
test('should return string starting with codebuff-cli-', () => {
81+
const fingerprint = generateFingerprintIdSync()
82+
expect(fingerprint.startsWith('codebuff-cli-')).toBe(true)
83+
})
84+
85+
test('should return fingerprint of expected length', () => {
86+
const fingerprint = generateFingerprintIdSync()
87+
// Format: codebuff-cli- (13 chars) + 8 random chars = 21 chars
88+
expect(fingerprint.length).toBe(21)
89+
})
90+
91+
test('should contain only valid base64url characters in suffix', () => {
92+
const fingerprint = generateFingerprintIdSync()
93+
const suffix = fingerprint.replace('codebuff-cli-', '')
94+
// base64url alphabet: A-Z, a-z, 0-9, -, _
95+
const base64urlPattern = /^[A-Za-z0-9_-]+$/
96+
expect(base64urlPattern.test(suffix)).toBe(true)
97+
})
98+
99+
test('should have exactly 8 characters in the random suffix', () => {
100+
const fingerprint = generateFingerprintIdSync()
101+
const suffix = fingerprint.replace('codebuff-cli-', '')
102+
expect(suffix.length).toBe(8)
103+
})
104+
})
105+
106+
describe('uniqueness', () => {
107+
test('should generate unique fingerprints across multiple calls', () => {
108+
const fingerprints = new Set<string>()
109+
const iterations = 100
110+
111+
for (let i = 0; i < iterations; i++) {
112+
fingerprints.add(generateFingerprintIdSync())
113+
}
114+
115+
// All fingerprints should be unique
116+
expect(fingerprints.size).toBe(iterations)
117+
})
118+
119+
test('should generate different fingerprints on consecutive calls', () => {
120+
const first = generateFingerprintIdSync()
121+
const second = generateFingerprintIdSync()
122+
const third = generateFingerprintIdSync()
123+
124+
expect(first).not.toBe(second)
125+
expect(second).not.toBe(third)
126+
expect(first).not.toBe(third)
127+
})
128+
})
129+
130+
describe('type detection integration', () => {
131+
test('should be detected as legacy by getFingerprintType', () => {
132+
const fingerprint = generateFingerprintIdSync()
133+
expect(getFingerprintType(fingerprint)).toBe('legacy')
134+
})
135+
136+
test('multiple generated fingerprints should all be detected as legacy', () => {
137+
for (let i = 0; i < 10; i++) {
138+
const fingerprint = generateFingerprintIdSync()
139+
expect(getFingerprintType(fingerprint)).toBe('legacy')
140+
}
141+
})
142+
})
143+
})
144+
})

0 commit comments

Comments
 (0)