Skip to content

Commit 5f054d1

Browse files
committed
add test
1 parent 5015f41 commit 5f054d1

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
import { describe, expect, it } from 'vitest';
5+
import { initializeLogger } from '../log.js';
6+
import { Future, delay } from '../utils.js';
7+
import { Agent } from './agent.js';
8+
import { AgentActivity } from './agent_activity.js';
9+
import { AgentSession } from './agent_session.js';
10+
import { SpeechHandle } from './speech_handle.js';
11+
12+
// Initialize logger for tests
13+
initializeLogger({ pretty: true, level: 'error' });
14+
15+
describe('AgentActivity - Issue #836: mainTask hanging when interrupting queued speech', () => {
16+
describe('Real AgentActivity integration', () => {
17+
it('should directly test mainTask with queue inspection', async () => {
18+
// Create AgentActivity with access to private methods
19+
const agent = new Agent({ instructions: 'Test agent' });
20+
const agentSession = new AgentSession({});
21+
const agentActivity = new AgentActivity(agent, agentSession);
22+
23+
await agentActivity.start();
24+
25+
// Access private members through casting
26+
const activity = agentActivity as any;
27+
28+
// Create speeches
29+
const speech1 = SpeechHandle.create();
30+
const speech2 = SpeechHandle.create();
31+
const speech3 = SpeechHandle.create();
32+
33+
// Interrupt speech2
34+
speech2.interrupt();
35+
36+
// Directly access and inspect the queue
37+
expect(activity.speechQueue.size()).toBe(0);
38+
39+
// Schedule speeches
40+
activity.scheduleSpeech(speech1, 5);
41+
activity.scheduleSpeech(speech2, 5);
42+
activity.scheduleSpeech(speech3, 5);
43+
44+
// Verify queue size
45+
expect(activity.speechQueue.size()).toBe(3);
46+
47+
// Mark generations done for non-interrupted speeches
48+
setTimeout(() => {
49+
if (!speech1.interrupted) speech1._markGenerationDone();
50+
}, 50);
51+
setTimeout(() => {
52+
if (!speech3.interrupted) speech3._markGenerationDone();
53+
}, 100);
54+
55+
// Wait for mainTask to process
56+
await delay(250);
57+
58+
// After processing, queue should be empty
59+
expect(activity.speechQueue.size()).toBe(0);
60+
61+
// Verify current speech is cleared
62+
expect(activity._currentSpeech).toBeUndefined();
63+
});
64+
65+
it('should test mainTask queue processing order with priorities', async () => {
66+
// Test that mainTask respects priority ordering
67+
const agent = new Agent({ instructions: 'Test agent' });
68+
const agentSession = new AgentSession({});
69+
const agentActivity = new AgentActivity(agent, agentSession);
70+
71+
await agentActivity.start();
72+
73+
const activity = agentActivity as any;
74+
75+
// Create speeches with different priorities
76+
const lowPriority = SpeechHandle.create();
77+
const normalPriority = SpeechHandle.create();
78+
const highPriority = SpeechHandle.create();
79+
80+
// Interrupt all to make processing fast
81+
lowPriority.interrupt();
82+
normalPriority.interrupt();
83+
highPriority.interrupt();
84+
85+
// Schedule in reverse priority order to test queue sorting
86+
activity.scheduleSpeech(lowPriority, 0); // SPEECH_PRIORITY_LOW
87+
activity.scheduleSpeech(normalPriority, 5); // SPEECH_PRIORITY_NORMAL
88+
activity.scheduleSpeech(highPriority, 10); // SPEECH_PRIORITY_HIGH
89+
90+
// Queue should have 3 items
91+
expect(activity.speechQueue.size()).toBe(3);
92+
93+
// Wait for mainTask to process
94+
await delay(200);
95+
96+
// Queue should be empty after processing
97+
expect(activity.speechQueue.size()).toBe(0);
98+
99+
// All speeches should be scheduled and interrupted
100+
[lowPriority, normalPriority, highPriority].forEach((s) => {
101+
expect(s.scheduled).toBe(true);
102+
expect(s.interrupted).toBe(true);
103+
});
104+
});
105+
106+
it('should verify mainTask does not hang with manual abort signal test', async () => {
107+
// This test manually calls mainTask and tests abort behavior
108+
const agent = new Agent({ instructions: 'Test agent' });
109+
const agentSession = new AgentSession({});
110+
const agentActivity = new AgentActivity(agent, agentSession);
111+
112+
// Don't start() - we'll manually set up for mainTask testing
113+
const activity = agentActivity as any;
114+
115+
// Create an abort controller to stop mainTask
116+
const abortController = new AbortController();
117+
118+
// Create interrupted speeches
119+
const speech1 = SpeechHandle.create();
120+
const speech2 = SpeechHandle.create();
121+
speech1.interrupt();
122+
speech2.interrupt();
123+
124+
// Manually add to queue
125+
activity.speechQueue.push([5, 1000, speech1]);
126+
activity.speechQueue.push([5, 2000, speech2]);
127+
activity.q_updated = new Future();
128+
activity.q_updated.resolve(); // Wake up mainTask
129+
130+
// Call mainTask directly with timeout protection
131+
const mainTaskPromise = activity.mainTask(abortController.signal);
132+
133+
// Let mainTask process the interrupted speeches
134+
await delay(100);
135+
136+
// Abort the mainTask
137+
abortController.abort();
138+
139+
// mainTask should exit cleanly without hanging
140+
const timeoutPromise = new Promise((_, reject) =>
141+
setTimeout(() => reject(new Error('mainTask did not exit after abort')), 1000),
142+
);
143+
144+
await expect(Promise.race([mainTaskPromise, timeoutPromise])).resolves.not.toThrow();
145+
146+
// Queue should be empty
147+
expect(activity.speechQueue.size()).toBe(0);
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)