condition-based-waiting-example.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // Complete implementation of condition-based waiting utilities
  2. // From: Lace test infrastructure improvements (2025-10-03)
  3. // Context: Fixed 15 flaky tests by replacing arbitrary timeouts
  4. import type { ThreadManager } from '~/threads/thread-manager';
  5. import type { LaceEvent, LaceEventType } from '~/threads/types';
  6. /**
  7. * Wait for a specific event type to appear in thread
  8. *
  9. * @param threadManager - The thread manager to query
  10. * @param threadId - Thread to check for events
  11. * @param eventType - Type of event to wait for
  12. * @param timeoutMs - Maximum time to wait (default 5000ms)
  13. * @returns Promise resolving to the first matching event
  14. *
  15. * Example:
  16. * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
  17. */
  18. export function waitForEvent(
  19. threadManager: ThreadManager,
  20. threadId: string,
  21. eventType: LaceEventType,
  22. timeoutMs = 5000
  23. ): Promise<LaceEvent> {
  24. return new Promise((resolve, reject) => {
  25. const startTime = Date.now();
  26. const check = () => {
  27. const events = threadManager.getEvents(threadId);
  28. const event = events.find((e) => e.type === eventType);
  29. if (event) {
  30. resolve(event);
  31. } else if (Date.now() - startTime > timeoutMs) {
  32. reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
  33. } else {
  34. setTimeout(check, 10); // Poll every 10ms for efficiency
  35. }
  36. };
  37. check();
  38. });
  39. }
  40. /**
  41. * Wait for a specific number of events of a given type
  42. *
  43. * @param threadManager - The thread manager to query
  44. * @param threadId - Thread to check for events
  45. * @param eventType - Type of event to wait for
  46. * @param count - Number of events to wait for
  47. * @param timeoutMs - Maximum time to wait (default 5000ms)
  48. * @returns Promise resolving to all matching events once count is reached
  49. *
  50. * Example:
  51. * // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
  52. * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
  53. */
  54. export function waitForEventCount(
  55. threadManager: ThreadManager,
  56. threadId: string,
  57. eventType: LaceEventType,
  58. count: number,
  59. timeoutMs = 5000
  60. ): Promise<LaceEvent[]> {
  61. return new Promise((resolve, reject) => {
  62. const startTime = Date.now();
  63. const check = () => {
  64. const events = threadManager.getEvents(threadId);
  65. const matchingEvents = events.filter((e) => e.type === eventType);
  66. if (matchingEvents.length >= count) {
  67. resolve(matchingEvents);
  68. } else if (Date.now() - startTime > timeoutMs) {
  69. reject(
  70. new Error(
  71. `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
  72. )
  73. );
  74. } else {
  75. setTimeout(check, 10);
  76. }
  77. };
  78. check();
  79. });
  80. }
  81. /**
  82. * Wait for an event matching a custom predicate
  83. * Useful when you need to check event data, not just type
  84. *
  85. * @param threadManager - The thread manager to query
  86. * @param threadId - Thread to check for events
  87. * @param predicate - Function that returns true when event matches
  88. * @param description - Human-readable description for error messages
  89. * @param timeoutMs - Maximum time to wait (default 5000ms)
  90. * @returns Promise resolving to the first matching event
  91. *
  92. * Example:
  93. * // Wait for TOOL_RESULT with specific ID
  94. * await waitForEventMatch(
  95. * threadManager,
  96. * agentThreadId,
  97. * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
  98. * 'TOOL_RESULT with id=call_123'
  99. * );
  100. */
  101. export function waitForEventMatch(
  102. threadManager: ThreadManager,
  103. threadId: string,
  104. predicate: (event: LaceEvent) => boolean,
  105. description: string,
  106. timeoutMs = 5000
  107. ): Promise<LaceEvent> {
  108. return new Promise((resolve, reject) => {
  109. const startTime = Date.now();
  110. const check = () => {
  111. const events = threadManager.getEvents(threadId);
  112. const event = events.find(predicate);
  113. if (event) {
  114. resolve(event);
  115. } else if (Date.now() - startTime > timeoutMs) {
  116. reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
  117. } else {
  118. setTimeout(check, 10);
  119. }
  120. };
  121. check();
  122. });
  123. }
  124. // Usage example from actual debugging session:
  125. //
  126. // BEFORE (flaky):
  127. // ---------------
  128. // const messagePromise = agent.sendMessage('Execute tools');
  129. // await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
  130. // agent.abort();
  131. // await messagePromise;
  132. // await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
  133. // expect(toolResults.length).toBe(2); // Fails randomly
  134. //
  135. // AFTER (reliable):
  136. // ----------------
  137. // const messagePromise = agent.sendMessage('Execute tools');
  138. // await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
  139. // agent.abort();
  140. // await messagePromise;
  141. // await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
  142. // expect(toolResults.length).toBe(2); // Always succeeds
  143. //
  144. // Result: 60% pass rate → 100%, 40% faster execution