Skip to content

Commit 0f220ec

Browse files
Reflexclaude
andcommitted
test(devbox): cover onEvict eviction monitor dispatch and teardown
Hermetic object-coverage test driving the shared EvictionMonitor via an in-memory fake watch_evictions stream: asserts onEvict fires once with (devbox, deadline), unmatched devbox ids are discarded, and cancelOnEvict aborts the stream. Gives the object-coverage suite 100% function coverage of the eviction wrapper without a live eviction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d8d6f4c commit 0f220ec

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Devbox } from '@runloop/api-client/sdk';
2+
import type { DevboxEvictionEventView } from '@runloop/api-client/resources/devboxes/devboxes';
3+
4+
// Hermetic tests: they drive the shared EvictionMonitor through an in-memory fake
5+
// SSE stream instead of a live devbox eviction, so `devbox.onEvict` / `cancelOnEvict`
6+
// (and the monitor's dispatch + teardown) are exercised deterministically.
7+
8+
// The generated watch_evictions stream: an async-iterable of eviction events plus an
9+
// AbortController the monitor aborts on close().
10+
type FakeStream = {
11+
controller: AbortController;
12+
[Symbol.asyncIterator](): AsyncIterator<DevboxEvictionEventView>;
13+
};
14+
15+
function streamOf(events: DevboxEvictionEventView[]): FakeStream {
16+
return {
17+
controller: new AbortController(),
18+
async *[Symbol.asyncIterator]() {
19+
for (const event of events) {
20+
yield event;
21+
}
22+
},
23+
};
24+
}
25+
26+
// A stream that emits nothing until the monitor aborts it (i.e. on cancelOnEvict).
27+
function blockingStream(): FakeStream {
28+
const controller = new AbortController();
29+
return {
30+
controller,
31+
async *[Symbol.asyncIterator]() {
32+
await new Promise<void>((resolve) => {
33+
if (controller.signal.aborted) {
34+
resolve();
35+
} else {
36+
controller.signal.addEventListener('abort', () => resolve(), { once: true });
37+
}
38+
});
39+
},
40+
};
41+
}
42+
43+
// Type the fake as whatever Devbox.fromId expects, without importing the generated client.
44+
type Client = Parameters<typeof Devbox.fromId>[0];
45+
function fakeClient(watchEvictions: () => Promise<FakeStream>): Client {
46+
return { devboxes: { watchEvictions } } as unknown as Client;
47+
}
48+
49+
const tick = (ms = 20) => new Promise((resolve) => setTimeout(resolve, ms));
50+
51+
(process.env['RUN_SMOKETESTS'] ? describe : describe.skip)('object-oriented eviction notifications', () => {
52+
test('onEvict fires once for a matching devbox and ignores others', async () => {
53+
const events: DevboxEvictionEventView[] = [
54+
{ devbox_id: 'dbx_other', eviction_deadline_ms: 1 },
55+
{ devbox_id: 'dbx_match', eviction_deadline_ms: 1_720_000_000_000 },
56+
];
57+
const devbox = Devbox.fromId(
58+
fakeClient(async () => streamOf(events)),
59+
'dbx_match',
60+
);
61+
62+
let receivedDevbox: Devbox | undefined;
63+
const calls: Array<{ id: string; deadline: number }> = [];
64+
const fired = new Promise<void>((resolve) => {
65+
devbox.onEvict((evicted, evictionDeadlineMs) => {
66+
receivedDevbox = evicted;
67+
calls.push({ id: evicted.id, deadline: evictionDeadlineMs });
68+
resolve();
69+
});
70+
});
71+
72+
let guard: ReturnType<typeof setTimeout>;
73+
const timeout = new Promise<never>((_resolve, reject) => {
74+
guard = setTimeout(() => reject(new Error('onEvict callback never fired')), 2000);
75+
});
76+
try {
77+
await Promise.race([fired, timeout]);
78+
} finally {
79+
clearTimeout(guard!);
80+
}
81+
await tick();
82+
83+
// Fired once, with the devbox object and its deadline; the unmatched id was discarded.
84+
expect(receivedDevbox).toBe(devbox);
85+
expect(calls).toEqual([{ id: 'dbx_match', deadline: 1_720_000_000_000 }]);
86+
});
87+
88+
test('cancelOnEvict stops watching and aborts the stream', async () => {
89+
const stream = blockingStream();
90+
const devbox = Devbox.fromId(
91+
fakeClient(async () => stream),
92+
'dbx_cancel',
93+
);
94+
95+
const callback = jest.fn();
96+
devbox.onEvict(callback);
97+
await tick(); // let the monitor open the stream
98+
99+
devbox.cancelOnEvict();
100+
101+
expect(stream.controller.signal.aborted).toBe(true);
102+
await tick();
103+
expect(callback).not.toHaveBeenCalled();
104+
});
105+
});

0 commit comments

Comments
 (0)