session-manager.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { describe, expect, test } from 'bun:test';
  2. import { deriveTaskSessionLabel, SessionManager } from './session-manager';
  3. describe('SessionManager', () => {
  4. test('keeps most recently used sessions within limit', () => {
  5. const manager = new SessionManager(2);
  6. manager.remember({
  7. parentSessionId: 'parent-1',
  8. taskId: 'task-1',
  9. agentType: 'explorer',
  10. label: 'first thread',
  11. });
  12. manager.remember({
  13. parentSessionId: 'parent-1',
  14. taskId: 'task-2',
  15. agentType: 'explorer',
  16. label: 'second thread',
  17. });
  18. manager.markUsed('parent-1', 'explorer', 'task-1');
  19. manager.remember({
  20. parentSessionId: 'parent-1',
  21. taskId: 'task-3',
  22. agentType: 'explorer',
  23. label: 'third thread',
  24. });
  25. const prompt = manager.formatForPrompt('parent-1');
  26. expect(prompt).toContain('exp-1 first thread');
  27. expect(prompt).toContain('exp-3 third thread');
  28. expect(prompt).not.toContain('exp-2 second thread');
  29. });
  30. test('clears parent-scoped sessions', () => {
  31. const manager = new SessionManager(2);
  32. manager.remember({
  33. parentSessionId: 'parent-1',
  34. taskId: 'task-1',
  35. agentType: 'oracle',
  36. label: 'architecture',
  37. });
  38. manager.clearParent('parent-1');
  39. expect(manager.formatForPrompt('parent-1')).toBeUndefined();
  40. });
  41. test('includes read context for remembered sessions', () => {
  42. const manager = new SessionManager(2);
  43. manager.remember({
  44. parentSessionId: 'parent-1',
  45. taskId: 'task-1',
  46. agentType: 'explorer',
  47. label: 'session manager',
  48. });
  49. manager.addContext('task-1', [
  50. { path: 'src/index.ts', lineCount: 42, lastReadAt: 1 },
  51. {
  52. path: 'src/multiplexer/session-manager.ts',
  53. lineCount: 24,
  54. lastReadAt: 2,
  55. },
  56. ]);
  57. const prompt = manager.formatForPrompt('parent-1');
  58. expect(prompt).toContain('exp-1 session manager');
  59. expect(prompt).toContain(
  60. 'Context read by exp-1: src/multiplexer/session-manager.ts (24 lines), src/index.ts (42 lines)',
  61. );
  62. });
  63. test('filters tiny reads and caps read context files', () => {
  64. const manager = new SessionManager(2);
  65. manager.remember({
  66. parentSessionId: 'parent-1',
  67. taskId: 'task-1',
  68. agentType: 'explorer',
  69. label: 'large context',
  70. });
  71. manager.addContext(
  72. 'task-1',
  73. Array.from({ length: 10 }, (_, index) => ({
  74. path: `file-${index}.ts`,
  75. lineCount: index === 0 ? 9 : 20 + index,
  76. lastReadAt: index,
  77. })),
  78. );
  79. const prompt = manager.formatForPrompt('parent-1') ?? '';
  80. expect(prompt).not.toContain('file-0.ts');
  81. expect(prompt).toContain('file-9.ts (29 lines)');
  82. expect(prompt).toContain('(+1 more)');
  83. });
  84. test('uses configurable read context thresholds', () => {
  85. const manager = new SessionManager(2, {
  86. readContextMinLines: 5,
  87. readContextMaxFiles: 1,
  88. });
  89. manager.remember({
  90. parentSessionId: 'parent-1',
  91. taskId: 'task-1',
  92. agentType: 'explorer',
  93. label: 'custom thresholds',
  94. });
  95. manager.addContext('task-1', [
  96. { path: 'small.ts', lineCount: 4, lastReadAt: 1 },
  97. { path: 'medium.ts', lineCount: 5, lastReadAt: 2 },
  98. { path: 'large.ts', lineCount: 12, lastReadAt: 3 },
  99. ]);
  100. const prompt = manager.formatForPrompt('parent-1') ?? '';
  101. expect(prompt).not.toContain('small.ts');
  102. expect(prompt).toContain('large.ts (12 lines)');
  103. expect(prompt).not.toContain('medium.ts');
  104. expect(prompt).toContain('(+1 more)');
  105. });
  106. test('bounds stored read context files to the render cap plus overflow marker', () => {
  107. const manager = new SessionManager(2, {
  108. readContextMinLines: 1,
  109. readContextMaxFiles: 2,
  110. });
  111. const remembered = manager.remember({
  112. parentSessionId: 'parent-1',
  113. taskId: 'task-1',
  114. agentType: 'explorer',
  115. label: 'bounded context',
  116. });
  117. manager.addContext(
  118. 'task-1',
  119. Array.from({ length: 10 }, (_, index) => ({
  120. path: `file-${index}.ts`,
  121. lineCount: 10,
  122. lastReadAt: index,
  123. })),
  124. );
  125. expect(remembered.contextFiles).toHaveLength(3);
  126. const prompt = manager.formatForPrompt('parent-1') ?? '';
  127. expect(prompt).toContain('file-9.ts (10 lines)');
  128. expect(prompt).toContain('file-8.ts (10 lines)');
  129. expect(prompt).toContain('(+1 more)');
  130. expect(prompt).not.toContain('file-0.ts');
  131. });
  132. });
  133. describe('deriveTaskSessionLabel', () => {
  134. test('prefers description over prompt', () => {
  135. expect(
  136. deriveTaskSessionLabel({
  137. description: 'config schema lookup',
  138. prompt: 'ignored prompt line',
  139. agentType: 'explorer',
  140. }),
  141. ).toBe('config schema lookup');
  142. });
  143. test('falls back to prompt then generic label', () => {
  144. expect(
  145. deriveTaskSessionLabel({
  146. prompt: '\n inspect task resumption support \nmore context',
  147. agentType: 'explorer',
  148. }),
  149. ).toBe('inspect task resumption support');
  150. expect(
  151. deriveTaskSessionLabel({
  152. agentType: 'fixer',
  153. }),
  154. ).toBe('recent fixer task');
  155. });
  156. });