loop-session.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { describe, expect, spyOn, test } from 'bun:test';
  2. import * as fs from 'node:fs';
  3. import {
  4. compactAttempt,
  5. createLoopSession,
  6. type LoopDefinition,
  7. loopDirname,
  8. writeHistoryFile,
  9. } from './loop-session';
  10. function testDef(overrides?: Partial<LoopDefinition>): LoopDefinition {
  11. return {
  12. goal: 'test goal',
  13. successCriteria: 'it works',
  14. success: { type: 'test', command: 'bun test' },
  15. maxAttempts: 3,
  16. executeAgent: 'fixer',
  17. verifyAgent: 'oracle',
  18. ...overrides,
  19. };
  20. }
  21. describe('loopDirname', () => {
  22. test('creates human-readable dir name with short ID', () => {
  23. const name = loopDirname('loop-mqwo5ddt', 'Fix typescript errors');
  24. expect(name).toBe('fix-typescript-errors-mqwo5ddt');
  25. });
  26. test('slugifies the goal text', () => {
  27. const name = loopDirname('loop-abc-123', 'Fix TypeScript & ESLint errors!');
  28. expect(name).toBe('fix-typescript-eslint-errors-123');
  29. });
  30. test('truncates long goals', () => {
  31. const longGoal = 'a'.repeat(50);
  32. const name = loopDirname('xyz-999', longGoal);
  33. expect(name.length).toBeLessThan(60);
  34. });
  35. });
  36. describe('createLoopSession', () => {
  37. test('creates a session with executing phase and attempt 1', () => {
  38. const def = testDef();
  39. const session = createLoopSession(def, 'loop-test-1');
  40. expect(session.loopID).toBe('loop-test-1');
  41. expect(session.definition).toBe(def);
  42. expect(session.currentPhase).toBe('executing');
  43. expect(session.attempts).toBe(1);
  44. expect(session.history).toEqual([]);
  45. expect(session.activeJobID).toBeUndefined();
  46. expect(session.manualReviewPending).toBe(false);
  47. expect(session.historyDir).toContain('test-goal');
  48. });
  49. });
  50. describe('compactAttempt', () => {
  51. test('formats a passed attempt', () => {
  52. const result = compactAttempt({
  53. attemptNumber: 1,
  54. executionResult: 'bun test',
  55. verificationResult: { passed: true, reason: 'all green' },
  56. });
  57. expect(result).toContain('## Attempt 1');
  58. expect(result).toContain('**Outcome:** PASS');
  59. expect(result).toContain('### Execution Result');
  60. });
  61. test('formats a failed attempt with reason', () => {
  62. const result = compactAttempt({
  63. attemptNumber: 2,
  64. executionResult: 'bun test',
  65. verificationResult: { passed: false, reason: 'tests failed' },
  66. });
  67. expect(result).toContain('## Attempt 2');
  68. expect(result).toContain('FAIL: tests failed');
  69. });
  70. test('includes artifacts when present', () => {
  71. const result = compactAttempt({
  72. attemptNumber: 1,
  73. executionResult: 'built',
  74. verificationResult: { passed: true, reason: 'ok' },
  75. artifactPaths: ['src/output.ts', 'src/output.test.ts'],
  76. });
  77. expect(result).toContain('artifacts: src/output.ts, src/output.test.ts');
  78. });
  79. });
  80. describe('writeHistoryFile', () => {
  81. test('uses the attempt number for the history filename', () => {
  82. const mkdirSpy = spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
  83. const writeSpy = spyOn(fs, 'writeFileSync').mockImplementation(
  84. () => undefined,
  85. );
  86. const session = createLoopSession(testDef(), 'loop-test-1');
  87. session.attempts = 99;
  88. session.history.push({
  89. attemptNumber: 4,
  90. executionResult: 'bun test',
  91. verificationResult: { passed: true, reason: 'ok' },
  92. });
  93. try {
  94. writeHistoryFile(session);
  95. expect(mkdirSpy).toHaveBeenCalledWith(session.historyDir, {
  96. recursive: true,
  97. });
  98. expect(writeSpy).toHaveBeenCalledWith(
  99. expect.stringContaining('history-004.md'),
  100. expect.stringContaining('## Attempt 4'),
  101. { encoding: 'utf-8' },
  102. );
  103. } finally {
  104. mkdirSpy.mockRestore();
  105. writeSpy.mockRestore();
  106. }
  107. });
  108. });