Skip to content

Commit 86c197a

Browse files
DaMandal0rianclaude
andcommitted
test: add comprehensive unit tests for performance modules
Add test suites for critical performance components: - network-cache.test.ts: LRU eviction, TTL expiration, cache warming, stale-while-revalidate, metrics tracking, disposal handling - circuit-breaker.test.ts: State transitions, failure thresholds, half-open recovery, batch execution, wrap function, disposal - allocation-priority-queue.test.ts: Priority ordering, batch operations, reprioritization, metrics tracking, memory bounds, disposal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6359911 commit 86c197a

File tree

3 files changed

+1012
-0
lines changed

3 files changed

+1012
-0
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import { AllocationPriorityQueue } from '../allocation-priority-queue'
2+
import { AllocationDecision } from '../../subgraphs'
3+
import { SubgraphDeploymentID } from '@graphprotocol/common-ts'
4+
import { BigNumber } from 'ethers'
5+
6+
// Mock logger
7+
const mockLogger = {
8+
child: jest.fn().mockReturnThis(),
9+
trace: jest.fn(),
10+
debug: jest.fn(),
11+
info: jest.fn(),
12+
warn: jest.fn(),
13+
error: jest.fn(),
14+
} as any
15+
16+
// Helper to create mock allocation decisions
17+
function createMockDecision(
18+
ipfsHash: string,
19+
toAllocate: boolean,
20+
options: {
21+
decisionBasis?: 'always' | 'rules' | 'offchain' | 'never'
22+
allocationAmount?: string
23+
safety?: boolean
24+
} = {},
25+
): AllocationDecision {
26+
return {
27+
deployment: new SubgraphDeploymentID(ipfsHash),
28+
toAllocate,
29+
protocolNetwork: 'arbitrum-one',
30+
reasonString: 'test',
31+
ruleMatch: {
32+
rule: {
33+
identifier: 'global',
34+
identifierType: 1,
35+
decisionBasis: options.decisionBasis || 'rules',
36+
allocationAmount: options.allocationAmount || '1000',
37+
safety: options.safety !== false,
38+
},
39+
protocolNetwork: 'arbitrum-one',
40+
},
41+
} as unknown as AllocationDecision
42+
}
43+
44+
describe('AllocationPriorityQueue', () => {
45+
let queue: AllocationPriorityQueue
46+
47+
beforeEach(() => {
48+
queue = new AllocationPriorityQueue(mockLogger)
49+
})
50+
51+
afterEach(() => {
52+
queue.dispose()
53+
})
54+
55+
describe('enqueue and dequeue', () => {
56+
it('should enqueue and dequeue items', () => {
57+
const decision = createMockDecision('QmTest1234567890123456789012345678901234567890', true)
58+
59+
queue.enqueue(decision)
60+
expect(queue.size()).toBe(1)
61+
62+
const dequeued = queue.dequeue()
63+
expect(dequeued).toBeDefined()
64+
expect(dequeued?.deployment.ipfsHash).toBe(decision.deployment.ipfsHash)
65+
expect(queue.size()).toBe(0)
66+
})
67+
68+
it('should return undefined when dequeuing from empty queue', () => {
69+
const result = queue.dequeue()
70+
expect(result).toBeUndefined()
71+
})
72+
73+
it('should prioritize allocations over deallocations', () => {
74+
const deallocate = createMockDecision(
75+
'QmDeal1234567890123456789012345678901234567890',
76+
false,
77+
)
78+
const allocate = createMockDecision(
79+
'QmAloc1234567890123456789012345678901234567890',
80+
true,
81+
)
82+
83+
// Enqueue deallocate first
84+
queue.enqueue(deallocate)
85+
queue.enqueue(allocate)
86+
87+
// Allocate should come out first (higher priority)
88+
const first = queue.dequeue()
89+
expect(first?.toAllocate).toBe(true)
90+
})
91+
92+
it('should prioritize "always" decisions over "rules" decisions', () => {
93+
const rulesDecision = createMockDecision(
94+
'QmRule1234567890123456789012345678901234567890',
95+
true,
96+
{ decisionBasis: 'rules' },
97+
)
98+
const alwaysDecision = createMockDecision(
99+
'QmAlwa1234567890123456789012345678901234567890',
100+
true,
101+
{ decisionBasis: 'always' },
102+
)
103+
104+
queue.enqueue(rulesDecision)
105+
queue.enqueue(alwaysDecision)
106+
107+
const first = queue.dequeue()
108+
expect(first?.ruleMatch.rule?.decisionBasis).toBe('always')
109+
})
110+
111+
it('should deprioritize unsafe deployments', () => {
112+
const safeDecision = createMockDecision(
113+
'QmSafe1234567890123456789012345678901234567890',
114+
true,
115+
{ safety: true },
116+
)
117+
const unsafeDecision = createMockDecision(
118+
'QmUnsa1234567890123456789012345678901234567890',
119+
true,
120+
{ safety: false },
121+
)
122+
123+
queue.enqueue(unsafeDecision)
124+
queue.enqueue(safeDecision)
125+
126+
const first = queue.dequeue()
127+
expect(first?.ruleMatch.rule?.safety).toBe(true)
128+
})
129+
})
130+
131+
describe('batch operations', () => {
132+
it('should enqueue batch efficiently', () => {
133+
const decisions = [
134+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
135+
createMockDecision('QmTest2234567890123456789012345678901234567890', true),
136+
createMockDecision('QmTest3234567890123456789012345678901234567890', false),
137+
]
138+
139+
queue.enqueueBatch(decisions)
140+
141+
expect(queue.size()).toBe(3)
142+
})
143+
144+
it('should dequeue batch in priority order', () => {
145+
const decisions = [
146+
createMockDecision('QmDeal1234567890123456789012345678901234567890', false),
147+
createMockDecision('QmAloc1234567890123456789012345678901234567890', true),
148+
createMockDecision('QmAlwa1234567890123456789012345678901234567890', true, {
149+
decisionBasis: 'always',
150+
}),
151+
]
152+
153+
queue.enqueueBatch(decisions)
154+
155+
const batch = queue.dequeueBatch(2)
156+
expect(batch).toHaveLength(2)
157+
158+
// Should get the two allocations first (both toAllocate=true)
159+
expect(batch.every((d) => d.toAllocate)).toBe(true)
160+
})
161+
162+
it('should handle empty batch enqueue', () => {
163+
queue.enqueueBatch([])
164+
expect(queue.size()).toBe(0)
165+
})
166+
167+
it('should handle dequeue batch larger than queue size', () => {
168+
const decision = createMockDecision(
169+
'QmTest1234567890123456789012345678901234567890',
170+
true,
171+
)
172+
queue.enqueue(decision)
173+
174+
const batch = queue.dequeueBatch(10)
175+
expect(batch).toHaveLength(1)
176+
})
177+
})
178+
179+
describe('peek', () => {
180+
it('should peek without removing', () => {
181+
const decision = createMockDecision(
182+
'QmTest1234567890123456789012345678901234567890',
183+
true,
184+
)
185+
queue.enqueue(decision)
186+
187+
const peeked = queue.peek()
188+
expect(peeked?.deployment.ipfsHash).toBe(decision.deployment.ipfsHash)
189+
expect(queue.size()).toBe(1)
190+
})
191+
192+
it('should return undefined on empty queue', () => {
193+
expect(queue.peek()).toBeUndefined()
194+
})
195+
196+
it('should peek batch without removing', () => {
197+
const decisions = [
198+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
199+
createMockDecision('QmTest2234567890123456789012345678901234567890', true),
200+
]
201+
queue.enqueueBatch(decisions)
202+
203+
const peeked = queue.peekBatch(1)
204+
expect(peeked).toHaveLength(1)
205+
expect(queue.size()).toBe(2)
206+
})
207+
})
208+
209+
describe('filter and remove', () => {
210+
it('should filter items by predicate', () => {
211+
const decisions = [
212+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
213+
createMockDecision('QmTest2234567890123456789012345678901234567890', false),
214+
createMockDecision('QmTest3234567890123456789012345678901234567890', true),
215+
]
216+
queue.enqueueBatch(decisions)
217+
218+
const allocations = queue.filter((d) => d.toAllocate)
219+
expect(allocations).toHaveLength(2)
220+
})
221+
222+
it('should remove items by predicate', () => {
223+
const decisions = [
224+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
225+
createMockDecision('QmTest2234567890123456789012345678901234567890', false),
226+
createMockDecision('QmTest3234567890123456789012345678901234567890', true),
227+
]
228+
queue.enqueueBatch(decisions)
229+
230+
const removed = queue.remove((d) => !d.toAllocate)
231+
expect(removed).toBe(1)
232+
expect(queue.size()).toBe(2)
233+
})
234+
})
235+
236+
describe('reprioritize', () => {
237+
it('should reprioritize existing item', () => {
238+
const lowPriority = createMockDecision(
239+
'QmLow11234567890123456789012345678901234567890',
240+
false,
241+
)
242+
const highPriority = createMockDecision(
243+
'QmHigh1234567890123456789012345678901234567890',
244+
true,
245+
)
246+
247+
queue.enqueue(highPriority)
248+
queue.enqueue(lowPriority)
249+
250+
// Boost low priority item
251+
const success = queue.reprioritize(
252+
'QmLow11234567890123456789012345678901234567890',
253+
(current) => current + 1000,
254+
)
255+
256+
expect(success).toBe(true)
257+
258+
// Now low priority item should be first
259+
const first = queue.dequeue()
260+
expect(first?.deployment.ipfsHash).toBe('QmLow11234567890123456789012345678901234567890')
261+
})
262+
263+
it('should return false for non-existent item', () => {
264+
const result = queue.reprioritize('nonexistent', (p) => p + 100)
265+
expect(result).toBe(false)
266+
})
267+
})
268+
269+
describe('has', () => {
270+
it('should check if deployment exists in queue', () => {
271+
const decision = createMockDecision(
272+
'QmTest1234567890123456789012345678901234567890',
273+
true,
274+
)
275+
queue.enqueue(decision)
276+
277+
expect(queue.has('QmTest1234567890123456789012345678901234567890')).toBe(true)
278+
expect(queue.has('nonexistent')).toBe(false)
279+
})
280+
})
281+
282+
describe('metrics', () => {
283+
it('should track metrics', () => {
284+
const decision = createMockDecision(
285+
'QmTest1234567890123456789012345678901234567890',
286+
true,
287+
)
288+
289+
queue.enqueue(decision)
290+
queue.dequeue()
291+
292+
const metrics = queue.getMetrics()
293+
expect(metrics.totalEnqueued).toBe(1)
294+
expect(metrics.totalDequeued).toBe(1)
295+
expect(metrics.currentSize).toBe(0)
296+
})
297+
298+
it('should track peak size', () => {
299+
const decisions = [
300+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
301+
createMockDecision('QmTest2234567890123456789012345678901234567890', true),
302+
createMockDecision('QmTest3234567890123456789012345678901234567890', true),
303+
]
304+
305+
queue.enqueueBatch(decisions)
306+
queue.dequeue()
307+
queue.dequeue()
308+
309+
const metrics = queue.getMetrics()
310+
expect(metrics.peakSize).toBe(3)
311+
expect(metrics.currentSize).toBe(1)
312+
})
313+
})
314+
315+
describe('getItems', () => {
316+
it('should return items with priorities and wait times', async () => {
317+
const decision = createMockDecision(
318+
'QmTest1234567890123456789012345678901234567890',
319+
true,
320+
)
321+
queue.enqueue(decision)
322+
323+
await new Promise((resolve) => setTimeout(resolve, 10))
324+
325+
const items = queue.getItems()
326+
expect(items).toHaveLength(1)
327+
expect(items[0].priority).toBeGreaterThan(0)
328+
expect(items[0].waitTime).toBeGreaterThanOrEqual(10)
329+
})
330+
})
331+
332+
describe('clear', () => {
333+
it('should clear all items', () => {
334+
const decisions = [
335+
createMockDecision('QmTest1234567890123456789012345678901234567890', true),
336+
createMockDecision('QmTest2234567890123456789012345678901234567890', true),
337+
]
338+
queue.enqueueBatch(decisions)
339+
340+
queue.clear()
341+
342+
expect(queue.size()).toBe(0)
343+
expect(queue.isEmpty()).toBe(true)
344+
})
345+
})
346+
347+
describe('disposal', () => {
348+
it('should throw when operating on disposed queue', () => {
349+
queue.dispose()
350+
351+
const decision = createMockDecision(
352+
'QmTest1234567890123456789012345678901234567890',
353+
true,
354+
)
355+
356+
expect(() => queue.enqueue(decision)).toThrow('disposed')
357+
expect(() => queue.dequeue()).toThrow('disposed')
358+
})
359+
360+
it('should be idempotent', () => {
361+
queue.dispose()
362+
queue.dispose() // Should not throw
363+
})
364+
})
365+
})

0 commit comments

Comments
 (0)