oh-my-claude-sisyphus 3.6.3 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +16 -0
  2. package/dist/__tests__/delegation-enforcement-levels.test.d.ts +9 -0
  3. package/dist/__tests__/delegation-enforcement-levels.test.d.ts.map +1 -0
  4. package/dist/__tests__/delegation-enforcement-levels.test.js +550 -0
  5. package/dist/__tests__/delegation-enforcement-levels.test.js.map +1 -0
  6. package/dist/__tests__/installer.test.js +1 -1
  7. package/dist/__tests__/rate-limit-wait/daemon.test.d.ts +5 -0
  8. package/dist/__tests__/rate-limit-wait/daemon.test.d.ts.map +1 -0
  9. package/dist/__tests__/rate-limit-wait/daemon.test.js +313 -0
  10. package/dist/__tests__/rate-limit-wait/daemon.test.js.map +1 -0
  11. package/dist/__tests__/rate-limit-wait/integration.test.d.ts +8 -0
  12. package/dist/__tests__/rate-limit-wait/integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/rate-limit-wait/integration.test.js +329 -0
  14. package/dist/__tests__/rate-limit-wait/integration.test.js.map +1 -0
  15. package/dist/__tests__/rate-limit-wait/rate-limit-monitor.test.d.ts +5 -0
  16. package/dist/__tests__/rate-limit-wait/rate-limit-monitor.test.d.ts.map +1 -0
  17. package/dist/__tests__/rate-limit-wait/rate-limit-monitor.test.js +167 -0
  18. package/dist/__tests__/rate-limit-wait/rate-limit-monitor.test.js.map +1 -0
  19. package/dist/__tests__/rate-limit-wait/tmux-detector.test.d.ts +5 -0
  20. package/dist/__tests__/rate-limit-wait/tmux-detector.test.d.ts.map +1 -0
  21. package/dist/__tests__/rate-limit-wait/tmux-detector.test.js +295 -0
  22. package/dist/__tests__/rate-limit-wait/tmux-detector.test.js.map +1 -0
  23. package/dist/cli/commands/wait.d.ts +52 -0
  24. package/dist/cli/commands/wait.d.ts.map +1 -0
  25. package/dist/cli/commands/wait.js +229 -0
  26. package/dist/cli/commands/wait.js.map +1 -0
  27. package/dist/cli/index.js +54 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/features/rate-limit-wait/daemon.d.ts +52 -0
  30. package/dist/features/rate-limit-wait/daemon.d.ts.map +1 -0
  31. package/dist/features/rate-limit-wait/daemon.js +545 -0
  32. package/dist/features/rate-limit-wait/daemon.js.map +1 -0
  33. package/dist/features/rate-limit-wait/index.d.ts +16 -0
  34. package/dist/features/rate-limit-wait/index.d.ts.map +1 -0
  35. package/dist/features/rate-limit-wait/index.js +18 -0
  36. package/dist/features/rate-limit-wait/index.js.map +1 -0
  37. package/dist/features/rate-limit-wait/rate-limit-monitor.d.ts +22 -0
  38. package/dist/features/rate-limit-wait/rate-limit-monitor.d.ts.map +1 -0
  39. package/dist/features/rate-limit-wait/rate-limit-monitor.js +99 -0
  40. package/dist/features/rate-limit-wait/rate-limit-monitor.js.map +1 -0
  41. package/dist/features/rate-limit-wait/tmux-detector.d.ts +59 -0
  42. package/dist/features/rate-limit-wait/tmux-detector.d.ts.map +1 -0
  43. package/dist/features/rate-limit-wait/tmux-detector.js +304 -0
  44. package/dist/features/rate-limit-wait/tmux-detector.js.map +1 -0
  45. package/dist/features/rate-limit-wait/types.d.ts +121 -0
  46. package/dist/features/rate-limit-wait/types.d.ts.map +1 -0
  47. package/dist/features/rate-limit-wait/types.js +8 -0
  48. package/dist/features/rate-limit-wait/types.js.map +1 -0
  49. package/dist/hooks/bridge.d.ts +1 -1
  50. package/dist/hooks/bridge.d.ts.map +1 -1
  51. package/dist/hooks/bridge.js +50 -4
  52. package/dist/hooks/bridge.js.map +1 -1
  53. package/dist/hooks/index.d.ts +5 -0
  54. package/dist/hooks/index.d.ts.map +1 -1
  55. package/dist/hooks/index.js +15 -0
  56. package/dist/hooks/index.js.map +1 -1
  57. package/dist/hooks/omc-orchestrator/audit.d.ts +2 -1
  58. package/dist/hooks/omc-orchestrator/audit.d.ts.map +1 -1
  59. package/dist/hooks/omc-orchestrator/audit.js.map +1 -1
  60. package/dist/hooks/omc-orchestrator/index.d.ts +7 -0
  61. package/dist/hooks/omc-orchestrator/index.d.ts.map +1 -1
  62. package/dist/hooks/omc-orchestrator/index.js +95 -8
  63. package/dist/hooks/omc-orchestrator/index.js.map +1 -1
  64. package/dist/hooks/permission-handler/__tests__/index.test.d.ts +2 -0
  65. package/dist/hooks/permission-handler/__tests__/index.test.d.ts.map +1 -0
  66. package/dist/hooks/permission-handler/__tests__/index.test.js +244 -0
  67. package/dist/hooks/permission-handler/__tests__/index.test.js.map +1 -0
  68. package/dist/hooks/permission-handler/index.d.ts +42 -0
  69. package/dist/hooks/permission-handler/index.d.ts.map +1 -0
  70. package/dist/hooks/permission-handler/index.js +111 -0
  71. package/dist/hooks/permission-handler/index.js.map +1 -0
  72. package/dist/hooks/pre-compact/index.d.ts +82 -0
  73. package/dist/hooks/pre-compact/index.d.ts.map +1 -0
  74. package/dist/hooks/pre-compact/index.js +265 -0
  75. package/dist/hooks/pre-compact/index.js.map +1 -0
  76. package/dist/hooks/session-end/index.d.ts +50 -0
  77. package/dist/hooks/session-end/index.d.ts.map +1 -0
  78. package/dist/hooks/session-end/index.js +207 -0
  79. package/dist/hooks/session-end/index.js.map +1 -0
  80. package/dist/hooks/setup/index.d.ts +66 -0
  81. package/dist/hooks/setup/index.d.ts.map +1 -0
  82. package/dist/hooks/setup/index.js +299 -0
  83. package/dist/hooks/setup/index.js.map +1 -0
  84. package/dist/hooks/setup/types.d.ts +25 -0
  85. package/dist/hooks/setup/types.d.ts.map +1 -0
  86. package/dist/hooks/setup/types.js +5 -0
  87. package/dist/hooks/setup/types.js.map +1 -0
  88. package/dist/hooks/subagent-tracker/index.d.ts +68 -29
  89. package/dist/hooks/subagent-tracker/index.d.ts.map +1 -1
  90. package/dist/hooks/subagent-tracker/index.js +301 -131
  91. package/dist/hooks/subagent-tracker/index.js.map +1 -1
  92. package/dist/installer/index.d.ts +1 -1
  93. package/dist/installer/index.js +1 -1
  94. package/hooks/hooks.json +83 -1
  95. package/package.json +3 -1
  96. package/scripts/permission-handler.mjs +23 -0
  97. package/scripts/pre-compact.mjs +23 -0
  98. package/scripts/session-end.mjs +23 -0
  99. package/scripts/setup-init.mjs +23 -0
  100. package/scripts/setup-maintenance.mjs +23 -0
  101. package/scripts/subagent-tracker.mjs +35 -0
  102. package/templates/hooks/keyword-detector.mjs +198 -0
  103. package/templates/hooks/keyword-detector.sh +102 -0
  104. package/templates/hooks/persistent-mode.mjs +249 -0
  105. package/templates/hooks/persistent-mode.sh +187 -0
  106. package/templates/hooks/post-tool-use.mjs +133 -0
  107. package/templates/hooks/post-tool-use.sh +90 -0
  108. package/templates/hooks/pre-tool-use.mjs +145 -0
  109. package/templates/hooks/pre-tool-use.sh +113 -0
  110. package/templates/hooks/session-start.mjs +100 -0
  111. package/templates/hooks/session-start.sh +62 -0
  112. package/templates/hooks/stop-continuation.mjs +80 -0
  113. package/templates/hooks/stop-continuation.sh +40 -0
  114. package/templates/rules/README.md +40 -0
  115. package/templates/rules/coding-style.md +74 -0
  116. package/templates/rules/git-workflow.md +41 -0
  117. package/templates/rules/performance.md +40 -0
  118. package/templates/rules/security.md +41 -0
  119. package/templates/rules/testing.md +42 -0
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Tests for daemon.ts
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { mkdirSync, writeFileSync, existsSync, rmSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+ import { readDaemonState, isDaemonRunning, getDaemonStatus, formatDaemonState, } from '../../features/rate-limit-wait/daemon.js';
9
+ describe('daemon', () => {
10
+ const testDir = join(tmpdir(), 'omc-daemon-test-' + Date.now());
11
+ const testConfig = {
12
+ stateFilePath: join(testDir, 'state.json'),
13
+ pidFilePath: join(testDir, 'daemon.pid'),
14
+ logFilePath: join(testDir, 'daemon.log'),
15
+ pollIntervalMs: 1000,
16
+ };
17
+ beforeEach(() => {
18
+ mkdirSync(testDir, { recursive: true });
19
+ });
20
+ afterEach(() => {
21
+ try {
22
+ rmSync(testDir, { recursive: true, force: true });
23
+ }
24
+ catch {
25
+ // Ignore cleanup errors
26
+ }
27
+ });
28
+ describe('readDaemonState', () => {
29
+ it('should return null when state file does not exist', () => {
30
+ const state = readDaemonState(testConfig);
31
+ expect(state).toBeNull();
32
+ });
33
+ it('should read and parse state file', () => {
34
+ const testState = {
35
+ isRunning: true,
36
+ pid: 1234,
37
+ startedAt: new Date('2024-01-01T00:00:00Z'),
38
+ lastPollAt: new Date('2024-01-01T00:01:00Z'),
39
+ rateLimitStatus: {
40
+ fiveHourLimited: false,
41
+ weeklyLimited: false,
42
+ isLimited: false,
43
+ fiveHourResetsAt: null,
44
+ weeklyResetsAt: null,
45
+ nextResetAt: null,
46
+ timeUntilResetMs: null,
47
+ lastCheckedAt: new Date('2024-01-01T00:01:00Z'),
48
+ },
49
+ blockedPanes: [],
50
+ resumedPaneIds: [],
51
+ totalResumeAttempts: 5,
52
+ successfulResumes: 3,
53
+ errorCount: 0,
54
+ };
55
+ writeFileSync(testConfig.stateFilePath, JSON.stringify(testState));
56
+ const state = readDaemonState(testConfig);
57
+ expect(state).not.toBeNull();
58
+ expect(state.isRunning).toBe(true);
59
+ expect(state.pid).toBe(1234);
60
+ expect(state.totalResumeAttempts).toBe(5);
61
+ expect(state.successfulResumes).toBe(3);
62
+ expect(state.startedAt).toBeInstanceOf(Date);
63
+ });
64
+ it('should handle invalid JSON gracefully', () => {
65
+ writeFileSync(testConfig.stateFilePath, 'invalid json{');
66
+ const state = readDaemonState(testConfig);
67
+ expect(state).toBeNull();
68
+ });
69
+ });
70
+ describe('isDaemonRunning', () => {
71
+ it('should return false when PID file does not exist', () => {
72
+ const running = isDaemonRunning(testConfig);
73
+ expect(running).toBe(false);
74
+ });
75
+ it('should return false for stale PID file', () => {
76
+ // Write a PID that definitely doesn't exist
77
+ writeFileSync(testConfig.pidFilePath, '999999');
78
+ const running = isDaemonRunning(testConfig);
79
+ expect(running).toBe(false);
80
+ // PID file should be cleaned up
81
+ expect(existsSync(testConfig.pidFilePath)).toBe(false);
82
+ });
83
+ it('should return true for current process PID', () => {
84
+ // Write current process PID
85
+ writeFileSync(testConfig.pidFilePath, String(process.pid));
86
+ const running = isDaemonRunning(testConfig);
87
+ expect(running).toBe(true);
88
+ });
89
+ });
90
+ describe('getDaemonStatus', () => {
91
+ it('should return not started status', () => {
92
+ const result = getDaemonStatus(testConfig);
93
+ expect(result.success).toBe(true);
94
+ expect(result.message).toBe('Daemon has never been started');
95
+ });
96
+ it('should return not running status when state exists but no PID', () => {
97
+ const testState = {
98
+ isRunning: false,
99
+ pid: null,
100
+ startedAt: new Date(),
101
+ lastPollAt: new Date(),
102
+ rateLimitStatus: null,
103
+ blockedPanes: [],
104
+ resumedPaneIds: [],
105
+ totalResumeAttempts: 0,
106
+ successfulResumes: 0,
107
+ errorCount: 0,
108
+ };
109
+ writeFileSync(testConfig.stateFilePath, JSON.stringify(testState));
110
+ const result = getDaemonStatus(testConfig);
111
+ expect(result.success).toBe(true);
112
+ expect(result.message).toBe('Daemon is not running');
113
+ expect(result.state).toBeDefined();
114
+ });
115
+ it('should return running status when PID file exists with valid PID', () => {
116
+ const testState = {
117
+ isRunning: true,
118
+ pid: process.pid,
119
+ startedAt: new Date(),
120
+ lastPollAt: new Date(),
121
+ rateLimitStatus: null,
122
+ blockedPanes: [],
123
+ resumedPaneIds: [],
124
+ totalResumeAttempts: 0,
125
+ successfulResumes: 0,
126
+ errorCount: 0,
127
+ };
128
+ writeFileSync(testConfig.stateFilePath, JSON.stringify(testState));
129
+ writeFileSync(testConfig.pidFilePath, String(process.pid));
130
+ const result = getDaemonStatus(testConfig);
131
+ expect(result.success).toBe(true);
132
+ expect(result.message).toBe('Daemon is running');
133
+ expect(result.state).toBeDefined();
134
+ });
135
+ });
136
+ describe('formatDaemonState', () => {
137
+ it('should format running daemon state', () => {
138
+ const state = {
139
+ isRunning: true,
140
+ pid: 1234,
141
+ startedAt: new Date(),
142
+ lastPollAt: new Date(),
143
+ rateLimitStatus: {
144
+ fiveHourLimited: false,
145
+ weeklyLimited: false,
146
+ isLimited: false,
147
+ fiveHourResetsAt: null,
148
+ weeklyResetsAt: null,
149
+ nextResetAt: null,
150
+ timeUntilResetMs: null,
151
+ lastCheckedAt: new Date(),
152
+ },
153
+ blockedPanes: [],
154
+ resumedPaneIds: [],
155
+ totalResumeAttempts: 10,
156
+ successfulResumes: 8,
157
+ errorCount: 2,
158
+ };
159
+ const output = formatDaemonState(state);
160
+ expect(output).toContain('Daemon running');
161
+ expect(output).toContain('PID: 1234');
162
+ expect(output).toContain('Not rate limited');
163
+ expect(output).toContain('Resume attempts: 10');
164
+ expect(output).toContain('Successful: 8');
165
+ expect(output).toContain('Errors: 2');
166
+ });
167
+ it('should format rate limited state', () => {
168
+ const state = {
169
+ isRunning: true,
170
+ pid: 1234,
171
+ startedAt: new Date(),
172
+ lastPollAt: new Date(),
173
+ rateLimitStatus: {
174
+ fiveHourLimited: true,
175
+ weeklyLimited: false,
176
+ isLimited: true,
177
+ fiveHourResetsAt: new Date(Date.now() + 3600000),
178
+ weeklyResetsAt: null,
179
+ nextResetAt: new Date(Date.now() + 3600000),
180
+ timeUntilResetMs: 3600000,
181
+ lastCheckedAt: new Date(),
182
+ },
183
+ blockedPanes: [],
184
+ resumedPaneIds: [],
185
+ totalResumeAttempts: 0,
186
+ successfulResumes: 0,
187
+ errorCount: 0,
188
+ };
189
+ const output = formatDaemonState(state);
190
+ expect(output).toContain('5-hour limit reached');
191
+ });
192
+ it('should format state with blocked panes', () => {
193
+ const state = {
194
+ isRunning: true,
195
+ pid: 1234,
196
+ startedAt: new Date(),
197
+ lastPollAt: new Date(),
198
+ rateLimitStatus: null,
199
+ blockedPanes: [
200
+ {
201
+ id: '%0',
202
+ session: 'main',
203
+ windowIndex: 0,
204
+ windowName: 'dev',
205
+ paneIndex: 0,
206
+ isActive: true,
207
+ analysis: {
208
+ hasClaudeCode: true,
209
+ hasRateLimitMessage: true,
210
+ isBlocked: true,
211
+ confidence: 0.9,
212
+ },
213
+ firstDetectedAt: new Date(),
214
+ resumeAttempted: false,
215
+ },
216
+ ],
217
+ resumedPaneIds: [],
218
+ totalResumeAttempts: 0,
219
+ successfulResumes: 0,
220
+ errorCount: 0,
221
+ };
222
+ const output = formatDaemonState(state);
223
+ expect(output).toContain('Found 1 blocked');
224
+ });
225
+ it('should format state with last error', () => {
226
+ const state = {
227
+ isRunning: true,
228
+ pid: 1234,
229
+ startedAt: new Date(),
230
+ lastPollAt: new Date(),
231
+ rateLimitStatus: null,
232
+ blockedPanes: [],
233
+ resumedPaneIds: [],
234
+ totalResumeAttempts: 0,
235
+ successfulResumes: 0,
236
+ errorCount: 1,
237
+ lastError: 'Test error message',
238
+ };
239
+ const output = formatDaemonState(state);
240
+ expect(output).toContain('Last error: Test error message');
241
+ });
242
+ it('should format not running state', () => {
243
+ const state = {
244
+ isRunning: false,
245
+ pid: null,
246
+ startedAt: null,
247
+ lastPollAt: null,
248
+ rateLimitStatus: null,
249
+ blockedPanes: [],
250
+ resumedPaneIds: [],
251
+ totalResumeAttempts: 0,
252
+ successfulResumes: 0,
253
+ errorCount: 0,
254
+ };
255
+ const output = formatDaemonState(state);
256
+ expect(output).toContain('Daemon not running');
257
+ });
258
+ });
259
+ describe('security: file permissions', () => {
260
+ it('should create state file with restrictive permissions', () => {
261
+ const testState = {
262
+ isRunning: true,
263
+ pid: 1234,
264
+ startedAt: new Date(),
265
+ lastPollAt: new Date(),
266
+ rateLimitStatus: null,
267
+ blockedPanes: [],
268
+ resumedPaneIds: [],
269
+ totalResumeAttempts: 0,
270
+ successfulResumes: 0,
271
+ errorCount: 0,
272
+ };
273
+ writeFileSync(testConfig.stateFilePath, JSON.stringify(testState));
274
+ // Read state back (this exercises the read path)
275
+ const state = readDaemonState(testConfig);
276
+ expect(state).not.toBeNull();
277
+ });
278
+ it('should not store sensitive data in state file', () => {
279
+ const testState = {
280
+ isRunning: true,
281
+ pid: 1234,
282
+ startedAt: new Date(),
283
+ lastPollAt: new Date(),
284
+ rateLimitStatus: {
285
+ fiveHourLimited: false,
286
+ weeklyLimited: false,
287
+ isLimited: false,
288
+ fiveHourResetsAt: null,
289
+ weeklyResetsAt: null,
290
+ nextResetAt: null,
291
+ timeUntilResetMs: null,
292
+ lastCheckedAt: new Date(),
293
+ },
294
+ blockedPanes: [],
295
+ resumedPaneIds: [],
296
+ totalResumeAttempts: 0,
297
+ successfulResumes: 0,
298
+ errorCount: 0,
299
+ };
300
+ writeFileSync(testConfig.stateFilePath, JSON.stringify(testState));
301
+ // Verify no tokens or credentials in state file
302
+ const { readFileSync } = require('fs');
303
+ const content = readFileSync(testConfig.stateFilePath, 'utf-8');
304
+ // State should not contain sensitive fields
305
+ expect(content).not.toContain('accessToken');
306
+ expect(content).not.toContain('apiKey');
307
+ expect(content).not.toContain('password');
308
+ expect(content).not.toContain('secret');
309
+ expect(content).not.toContain('credential');
310
+ });
311
+ });
312
+ });
313
+ //# sourceMappingURL=daemon.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.test.js","sourceRoot":"","sources":["../../../src/__tests__/rate-limit-wait/daemon.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAM,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAc,UAAU,EAAE,MAAM,EAAY,MAAM,IAAI,CAAC;AACxF,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,iBAAiB,GAClB,MAAM,0CAA0C,CAAC;AAGlD,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAChE,MAAM,UAAU,GAAiB;QAC/B,aAAa,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC;QAC1C,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC;QACxC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC;QACxC,cAAc,EAAE,IAAI;KACrB,CAAC;IAEF,UAAU,CAAC,GAAG,EAAE;QACd,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC;YACH,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,wBAAwB;QAC1B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;YAC3D,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,SAAS,GAAgB;gBAC7B,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;gBAC3C,UAAU,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;gBAC5C,eAAe,EAAE;oBACf,eAAe,EAAE,KAAK;oBACtB,aAAa,EAAE,KAAK;oBACpB,SAAS,EAAE,KAAK;oBAChB,gBAAgB,EAAE,IAAI;oBACtB,cAAc,EAAE,IAAI;oBACpB,WAAW,EAAE,IAAI;oBACjB,gBAAgB,EAAE,IAAI;oBACtB,aAAa,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;iBAChD;gBACD,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;YAEpE,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE1C,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC7B,MAAM,CAAC,KAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,CAAC,KAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,CAAC,KAAM,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,CAAC,KAAM,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACzC,MAAM,CAAC,KAAM,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;YAC/C,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,eAAe,CAAC,CAAC;YAE1D,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE1C,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;YAC1D,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC5C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,4CAA4C;YAC5C,aAAa,CAAC,UAAU,CAAC,WAAY,EAAE,QAAQ,CAAC,CAAC;YAEjD,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE5C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5B,gCAAgC;YAChC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,WAAY,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;YACpD,4BAA4B;YAC5B,aAAa,CAAC,UAAU,CAAC,WAAY,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAE5D,MAAM,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE5C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE3C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;YACvE,MAAM,SAAS,GAAgB;gBAC7B,SAAS,EAAE,KAAK;gBAChB,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;YAEpE,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE3C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;YACrD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACrC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;YAC1E,MAAM,SAAS,GAAgB;gBAC7B,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;YACpE,aAAa,CAAC,UAAU,CAAC,WAAY,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAE5D,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAE3C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YACjD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE;oBACf,eAAe,EAAE,KAAK;oBACtB,aAAa,EAAE,KAAK;oBACpB,SAAS,EAAE,KAAK;oBAChB,gBAAgB,EAAE,IAAI;oBACtB,cAAc,EAAE,IAAI;oBACpB,WAAW,EAAE,IAAI;oBACjB,gBAAgB,EAAE,IAAI;oBACtB,aAAa,EAAE,IAAI,IAAI,EAAE;iBAC1B;gBACD,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,EAAE;gBACvB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAExC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;YACtC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;YAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAC;YAChD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;YAC1C,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE;oBACf,eAAe,EAAE,IAAI;oBACrB,aAAa,EAAE,KAAK;oBACpB,SAAS,EAAE,IAAI;oBACf,gBAAgB,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;oBAChD,cAAc,EAAE,IAAI;oBACpB,WAAW,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;oBAC3C,gBAAgB,EAAE,OAAO;oBACzB,aAAa,EAAE,IAAI,IAAI,EAAE;iBAC1B;gBACD,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAExC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE;oBACZ;wBACE,EAAE,EAAE,IAAI;wBACR,OAAO,EAAE,MAAM;wBACf,WAAW,EAAE,CAAC;wBACd,UAAU,EAAE,KAAK;wBACjB,SAAS,EAAE,CAAC;wBACZ,QAAQ,EAAE,IAAI;wBACd,QAAQ,EAAE;4BACR,aAAa,EAAE,IAAI;4BACnB,mBAAmB,EAAE,IAAI;4BACzB,SAAS,EAAE,IAAI;4BACf,UAAU,EAAE,GAAG;yBAChB;wBACD,eAAe,EAAE,IAAI,IAAI,EAAE;wBAC3B,eAAe,EAAE,KAAK;qBACvB;iBACF;gBACD,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAExC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;gBACb,SAAS,EAAE,oBAAoB;aAChC,CAAC;YAEF,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAExC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;YACzC,MAAM,KAAK,GAAgB;gBACzB,SAAS,EAAE,KAAK;gBAChB,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI;gBACf,UAAU,EAAE,IAAI;gBAChB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAExC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;QAC1C,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;YAC/D,MAAM,SAAS,GAAgB;gBAC7B,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE,IAAI;gBACrB,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;YAEpE,iDAAiD;YACjD,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;YACvD,MAAM,SAAS,GAAgB;gBAC7B,SAAS,EAAE,IAAI;gBACf,GAAG,EAAE,IAAI;gBACT,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,eAAe,EAAE;oBACf,eAAe,EAAE,KAAK;oBACtB,aAAa,EAAE,KAAK;oBACpB,SAAS,EAAE,KAAK;oBAChB,gBAAgB,EAAE,IAAI;oBACtB,cAAc,EAAE,IAAI;oBACpB,WAAW,EAAE,IAAI;oBACjB,gBAAgB,EAAE,IAAI;oBACtB,aAAa,EAAE,IAAI,IAAI,EAAE;iBAC1B;gBACD,YAAY,EAAE,EAAE;gBAChB,cAAc,EAAE,EAAE;gBAClB,mBAAmB,EAAE,CAAC;gBACtB,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;aACd,CAAC;YAEF,aAAa,CAAC,UAAU,CAAC,aAAc,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;YAEpE,gDAAgD;YAChD,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG,YAAY,CAAC,UAAU,CAAC,aAAc,EAAE,OAAO,CAAC,CAAC;YAEjE,4CAA4C;YAC5C,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACxC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;YAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACxC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Integration Tests for Rate Limit Wait Feature
3
+ *
4
+ * These tests simulate real-world scenarios without hitting actual rate limits.
5
+ * They verify the full flow from detection to resume.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/rate-limit-wait/integration.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Integration Tests for Rate Limit Wait Feature
3
+ *
4
+ * These tests simulate real-world scenarios without hitting actual rate limits.
5
+ * They verify the full flow from detection to resume.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { mkdirSync, rmSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { tmpdir } from 'os';
11
+ // Mock modules
12
+ vi.mock('../../hud/usage-api.js', () => ({
13
+ getUsage: vi.fn(),
14
+ }));
15
+ vi.mock('child_process', () => ({
16
+ execSync: vi.fn(),
17
+ spawnSync: vi.fn(),
18
+ spawn: vi.fn(),
19
+ }));
20
+ import { getUsage } from '../../hud/usage-api.js';
21
+ import { execSync, spawnSync } from 'child_process';
22
+ import { checkRateLimitStatus, analyzePaneContent, scanForBlockedPanes, formatDaemonState, } from '../../features/rate-limit-wait/index.js';
23
+ describe('Rate Limit Wait Integration Tests', () => {
24
+ const testDir = join(tmpdir(), 'omc-integration-test-' + Date.now());
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ mkdirSync(testDir, { recursive: true });
28
+ });
29
+ afterEach(() => {
30
+ try {
31
+ rmSync(testDir, { recursive: true, force: true });
32
+ }
33
+ catch {
34
+ // Ignore cleanup errors
35
+ }
36
+ });
37
+ describe('Scenario: Rate limit detection and tracking', () => {
38
+ it('should detect when 5-hour limit is reached', async () => {
39
+ // Simulate rate limit API response
40
+ vi.mocked(getUsage).mockResolvedValue({
41
+ fiveHourPercent: 100,
42
+ weeklyPercent: 75,
43
+ fiveHourResetsAt: new Date(Date.now() + 3600000),
44
+ weeklyResetsAt: null,
45
+ });
46
+ const status = await checkRateLimitStatus();
47
+ expect(status).not.toBeNull();
48
+ expect(status.isLimited).toBe(true);
49
+ expect(status.fiveHourLimited).toBe(true);
50
+ expect(status.weeklyLimited).toBe(false);
51
+ expect(status.timeUntilResetMs).toBeGreaterThan(0);
52
+ expect(status.timeUntilResetMs).toBeLessThanOrEqual(3600000);
53
+ });
54
+ it('should detect when weekly limit is reached', async () => {
55
+ vi.mocked(getUsage).mockResolvedValue({
56
+ fiveHourPercent: 50,
57
+ weeklyPercent: 100,
58
+ fiveHourResetsAt: null,
59
+ weeklyResetsAt: new Date(Date.now() + 86400000),
60
+ });
61
+ const status = await checkRateLimitStatus();
62
+ expect(status).not.toBeNull();
63
+ expect(status.isLimited).toBe(true);
64
+ expect(status.fiveHourLimited).toBe(false);
65
+ expect(status.weeklyLimited).toBe(true);
66
+ });
67
+ it('should handle transition from limited to not limited', async () => {
68
+ // First call: limited
69
+ vi.mocked(getUsage).mockResolvedValueOnce({
70
+ fiveHourPercent: 100,
71
+ weeklyPercent: 50,
72
+ fiveHourResetsAt: new Date(Date.now() + 1000),
73
+ weeklyResetsAt: null,
74
+ });
75
+ const limitedStatus = await checkRateLimitStatus();
76
+ expect(limitedStatus.isLimited).toBe(true);
77
+ // Second call: no longer limited
78
+ vi.mocked(getUsage).mockResolvedValueOnce({
79
+ fiveHourPercent: 0,
80
+ weeklyPercent: 50,
81
+ fiveHourResetsAt: null,
82
+ weeklyResetsAt: null,
83
+ });
84
+ const clearedStatus = await checkRateLimitStatus();
85
+ expect(clearedStatus.isLimited).toBe(false);
86
+ });
87
+ });
88
+ describe('Scenario: tmux pane analysis accuracy', () => {
89
+ it('should correctly identify Claude Code rate limit message', () => {
90
+ const realWorldContent = `
91
+ ╭─────────────────────────────────────────────────────────────────╮
92
+ │ Claude Code │
93
+ ╰─────────────────────────────────────────────────────────────────╯
94
+
95
+ You've reached your usage limit for the 5-hour period.
96
+ Your limit will reset at 3:45 PM.
97
+
98
+ What would you like to do?
99
+
100
+ [1] Wait and continue automatically when limit resets
101
+ [2] Switch to a different conversation
102
+ [3] Exit
103
+
104
+ > `;
105
+ const result = analyzePaneContent(realWorldContent);
106
+ expect(result.hasClaudeCode).toBe(true);
107
+ expect(result.hasRateLimitMessage).toBe(true);
108
+ expect(result.isBlocked).toBe(true);
109
+ expect(result.rateLimitType).toBe('five_hour');
110
+ expect(result.confidence).toBeGreaterThanOrEqual(0.8);
111
+ });
112
+ it('should correctly identify weekly rate limit message', () => {
113
+ const weeklyLimitContent = `
114
+ Claude Code v1.0.0
115
+
116
+ ⚠️ Weekly usage limit reached
117
+
118
+ You've used your weekly allocation of tokens.
119
+ Limit resets on Monday at 12:00 AM UTC.
120
+
121
+ Options:
122
+ [1] Continue when limit resets
123
+ [2] Exit
124
+
125
+ Enter choice: `;
126
+ const result = analyzePaneContent(weeklyLimitContent);
127
+ expect(result.hasClaudeCode).toBe(true);
128
+ expect(result.hasRateLimitMessage).toBe(true);
129
+ expect(result.isBlocked).toBe(true);
130
+ expect(result.rateLimitType).toBe('weekly');
131
+ });
132
+ it('should NOT flag normal Claude Code output as blocked', () => {
133
+ const normalContent = `
134
+ Claude Code
135
+
136
+ > What would you like to build today?
137
+
138
+ I can help you with:
139
+ - Writing code
140
+ - Debugging
141
+ - Refactoring
142
+ - Documentation
143
+
144
+ Just describe what you need!
145
+ `;
146
+ const result = analyzePaneContent(normalContent);
147
+ expect(result.hasClaudeCode).toBe(true);
148
+ expect(result.hasRateLimitMessage).toBe(false);
149
+ expect(result.isBlocked).toBe(false);
150
+ });
151
+ it('should NOT flag unrelated rate limit messages', () => {
152
+ const unrelatedContent = `
153
+ $ curl https://api.github.com/users/test
154
+ {
155
+ "message": "API rate limit exceeded for IP",
156
+ "documentation_url": "https://docs.github.com"
157
+ }
158
+ $ `;
159
+ const result = analyzePaneContent(unrelatedContent);
160
+ expect(result.hasClaudeCode).toBe(false);
161
+ expect(result.hasRateLimitMessage).toBe(true);
162
+ expect(result.isBlocked).toBe(false); // No Claude context
163
+ });
164
+ it('should handle edge case: old rate limit message scrolled up', () => {
165
+ // Only last 15 lines should be analyzed
166
+ // Rate limit message from earlier should be ignored if not in recent content
167
+ const scrolledContent = `
168
+ User: fix the bug
169
+ Assistant: I'll fix that for you.
170
+ [Edit] src/main.ts
171
+ Done! The bug is fixed.
172
+
173
+ User: thanks
174
+ Assistant: You're welcome!
175
+
176
+ User: what else?
177
+ Assistant: I can help with more tasks.
178
+
179
+ > `;
180
+ const result = analyzePaneContent(scrolledContent);
181
+ expect(result.isBlocked).toBe(false);
182
+ });
183
+ });
184
+ describe('Scenario: Full daemon state lifecycle', () => {
185
+ it('should format daemon state correctly for user display', () => {
186
+ const state = {
187
+ isRunning: true,
188
+ pid: 12345,
189
+ startedAt: new Date('2024-01-01T10:00:00Z'),
190
+ lastPollAt: new Date('2024-01-01T10:05:00Z'),
191
+ rateLimitStatus: {
192
+ fiveHourLimited: true,
193
+ weeklyLimited: false,
194
+ isLimited: true,
195
+ fiveHourResetsAt: new Date('2024-01-01T15:00:00Z'),
196
+ weeklyResetsAt: null,
197
+ nextResetAt: new Date('2024-01-01T15:00:00Z'),
198
+ timeUntilResetMs: 3600000,
199
+ lastCheckedAt: new Date('2024-01-01T10:05:00Z'),
200
+ },
201
+ blockedPanes: [
202
+ {
203
+ id: '%0',
204
+ session: 'dev',
205
+ windowIndex: 0,
206
+ windowName: 'claude',
207
+ paneIndex: 0,
208
+ isActive: true,
209
+ analysis: {
210
+ hasClaudeCode: true,
211
+ hasRateLimitMessage: true,
212
+ isBlocked: true,
213
+ rateLimitType: 'five_hour',
214
+ confidence: 0.95,
215
+ },
216
+ firstDetectedAt: new Date('2024-01-01T10:01:00Z'),
217
+ resumeAttempted: false,
218
+ },
219
+ ],
220
+ resumedPaneIds: [],
221
+ totalResumeAttempts: 0,
222
+ successfulResumes: 0,
223
+ errorCount: 0,
224
+ };
225
+ const output = formatDaemonState(state);
226
+ // Verify key information is present
227
+ expect(output).toContain('Daemon running');
228
+ expect(output).toContain('12345');
229
+ expect(output).toContain('5-hour limit');
230
+ expect(output).toContain('Found 1 blocked');
231
+ expect(output).toContain('%0');
232
+ });
233
+ it('should track resume attempts correctly', () => {
234
+ const stateAfterResume = {
235
+ isRunning: true,
236
+ pid: 12345,
237
+ startedAt: new Date(),
238
+ lastPollAt: new Date(),
239
+ rateLimitStatus: {
240
+ fiveHourLimited: false,
241
+ weeklyLimited: false,
242
+ isLimited: false,
243
+ fiveHourResetsAt: null,
244
+ weeklyResetsAt: null,
245
+ nextResetAt: null,
246
+ timeUntilResetMs: null,
247
+ lastCheckedAt: new Date(),
248
+ },
249
+ blockedPanes: [],
250
+ resumedPaneIds: ['%0', '%1'],
251
+ totalResumeAttempts: 2,
252
+ successfulResumes: 2,
253
+ errorCount: 0,
254
+ };
255
+ const output = formatDaemonState(stateAfterResume);
256
+ expect(output).toContain('Resume attempts: 2');
257
+ expect(output).toContain('Successful: 2');
258
+ expect(output).toContain('Not rate limited');
259
+ });
260
+ });
261
+ describe('Scenario: Error handling and edge cases', () => {
262
+ it('should handle OAuth credentials not available', async () => {
263
+ vi.mocked(getUsage).mockResolvedValue(null);
264
+ const status = await checkRateLimitStatus();
265
+ expect(status).toBeNull();
266
+ });
267
+ it('should handle API timeout gracefully', async () => {
268
+ vi.mocked(getUsage).mockRejectedValue(new Error('ETIMEDOUT'));
269
+ const status = await checkRateLimitStatus();
270
+ expect(status).toBeNull();
271
+ });
272
+ it('should handle tmux not installed', () => {
273
+ vi.mocked(spawnSync).mockReturnValue({
274
+ status: 1,
275
+ stdout: '',
276
+ stderr: 'tmux: command not found',
277
+ signal: null,
278
+ pid: 0,
279
+ output: [],
280
+ });
281
+ // scanForBlockedPanes should return empty array, not throw
282
+ const blocked = scanForBlockedPanes();
283
+ expect(blocked).toEqual([]);
284
+ });
285
+ it('should handle malformed tmux output', () => {
286
+ vi.mocked(spawnSync).mockReturnValue({
287
+ status: 0,
288
+ stdout: '/usr/bin/tmux',
289
+ stderr: '',
290
+ signal: null,
291
+ pid: 1234,
292
+ output: [],
293
+ });
294
+ vi.mocked(execSync).mockReturnValue('malformed output without proper format');
295
+ // Should not throw, just return empty
296
+ const blocked = scanForBlockedPanes();
297
+ expect(blocked).toEqual([]);
298
+ });
299
+ });
300
+ describe('Scenario: Confidence scoring', () => {
301
+ it('should give higher confidence for multiple indicators', () => {
302
+ const highConfidenceContent = `
303
+ Claude Code
304
+ Rate limit reached
305
+ 5-hour usage limit
306
+ [1] Continue
307
+ [2] Exit
308
+ `;
309
+ const lowConfidenceContent = `
310
+ Claude
311
+ rate limit
312
+ `;
313
+ const highResult = analyzePaneContent(highConfidenceContent);
314
+ const lowResult = analyzePaneContent(lowConfidenceContent);
315
+ expect(highResult.confidence).toBeGreaterThan(lowResult.confidence);
316
+ });
317
+ it('should require minimum confidence to mark as blocked', () => {
318
+ const ambiguousContent = `
319
+ some claude reference
320
+ limit mentioned
321
+ `;
322
+ const result = analyzePaneContent(ambiguousContent);
323
+ // Even if some patterns match, confidence should be too low
324
+ expect(result.confidence).toBeLessThan(0.6);
325
+ expect(result.isBlocked).toBe(false);
326
+ });
327
+ });
328
+ });
329
+ //# sourceMappingURL=integration.test.js.map