@agent-relay/wrapper 0.1.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.
- package/dist/__fixtures__/claude-outputs.d.ts +49 -0
- package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/claude-outputs.js +443 -0
- package/dist/__fixtures__/claude-outputs.js.map +1 -0
- package/dist/__fixtures__/codex-outputs.d.ts +9 -0
- package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/codex-outputs.js +94 -0
- package/dist/__fixtures__/codex-outputs.js.map +1 -0
- package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
- package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/gemini-outputs.js +144 -0
- package/dist/__fixtures__/gemini-outputs.js.map +1 -0
- package/dist/__fixtures__/index.d.ts +68 -0
- package/dist/__fixtures__/index.d.ts.map +1 -0
- package/dist/__fixtures__/index.js +44 -0
- package/dist/__fixtures__/index.js.map +1 -0
- package/dist/auth-detection.d.ts +49 -0
- package/dist/auth-detection.d.ts.map +1 -0
- package/dist/auth-detection.js +199 -0
- package/dist/auth-detection.js.map +1 -0
- package/dist/base-wrapper.d.ts +225 -0
- package/dist/base-wrapper.d.ts.map +1 -0
- package/dist/base-wrapper.js +572 -0
- package/dist/base-wrapper.js.map +1 -0
- package/dist/client.d.ts +254 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +801 -0
- package/dist/client.js.map +1 -0
- package/dist/id-generator.d.ts +35 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +60 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/idle-detector.d.ts +110 -0
- package/dist/idle-detector.d.ts.map +1 -0
- package/dist/idle-detector.js +304 -0
- package/dist/idle-detector.js.map +1 -0
- package/dist/inbox.d.ts +37 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +73 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +236 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1238 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompt-composer.d.ts +67 -0
- package/dist/prompt-composer.d.ts.map +1 -0
- package/dist/prompt-composer.js +168 -0
- package/dist/prompt-composer.js.map +1 -0
- package/dist/relay-pty-orchestrator.d.ts +407 -0
- package/dist/relay-pty-orchestrator.d.ts.map +1 -0
- package/dist/relay-pty-orchestrator.js +1885 -0
- package/dist/relay-pty-orchestrator.js.map +1 -0
- package/dist/shared.d.ts +201 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +341 -0
- package/dist/shared.js.map +1 -0
- package/dist/stuck-detector.d.ts +161 -0
- package/dist/stuck-detector.d.ts.map +1 -0
- package/dist/stuck-detector.js +402 -0
- package/dist/stuck-detector.js.map +1 -0
- package/dist/tmux-resolver.d.ts +55 -0
- package/dist/tmux-resolver.d.ts.map +1 -0
- package/dist/tmux-resolver.js +175 -0
- package/dist/tmux-resolver.js.map +1 -0
- package/dist/tmux-wrapper.d.ts +345 -0
- package/dist/tmux-wrapper.d.ts.map +1 -0
- package/dist/tmux-wrapper.js +1747 -0
- package/dist/tmux-wrapper.js.map +1 -0
- package/dist/trajectory-integration.d.ts +292 -0
- package/dist/trajectory-integration.d.ts.map +1 -0
- package/dist/trajectory-integration.js +979 -0
- package/dist/trajectory-integration.js.map +1 -0
- package/dist/wrapper-types.d.ts +41 -0
- package/dist/wrapper-types.d.ts.map +1 -0
- package/dist/wrapper-types.js +7 -0
- package/dist/wrapper-types.js.map +1 -0
- package/package.json +63 -0
- package/src/__fixtures__/claude-outputs.ts +471 -0
- package/src/__fixtures__/codex-outputs.ts +99 -0
- package/src/__fixtures__/gemini-outputs.ts +151 -0
- package/src/__fixtures__/index.ts +47 -0
- package/src/auth-detection.ts +244 -0
- package/src/base-wrapper.test.ts +540 -0
- package/src/base-wrapper.ts +741 -0
- package/src/client.test.ts +262 -0
- package/src/client.ts +984 -0
- package/src/id-generator.test.ts +71 -0
- package/src/id-generator.ts +69 -0
- package/src/idle-detector.test.ts +390 -0
- package/src/idle-detector.ts +370 -0
- package/src/inbox.test.ts +233 -0
- package/src/inbox.ts +89 -0
- package/src/index.ts +170 -0
- package/src/parser.regression.test.ts +251 -0
- package/src/parser.test.ts +1359 -0
- package/src/parser.ts +1477 -0
- package/src/prompt-composer.test.ts +219 -0
- package/src/prompt-composer.ts +231 -0
- package/src/relay-pty-orchestrator.test.ts +1027 -0
- package/src/relay-pty-orchestrator.ts +2270 -0
- package/src/shared.test.ts +221 -0
- package/src/shared.ts +454 -0
- package/src/stuck-detector.test.ts +303 -0
- package/src/stuck-detector.ts +511 -0
- package/src/tmux-resolver.test.ts +104 -0
- package/src/tmux-resolver.ts +207 -0
- package/src/tmux-wrapper.test.ts +316 -0
- package/src/tmux-wrapper.ts +2010 -0
- package/src/trajectory-detection.test.ts +151 -0
- package/src/trajectory-integration.ts +1261 -0
- package/src/wrapper-types.ts +45 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID Generator Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
6
|
+
import { IdGenerator, idGen, generateId } from './id-generator.js';
|
|
7
|
+
|
|
8
|
+
describe('IdGenerator', () => {
|
|
9
|
+
let generator: IdGenerator;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
generator = new IdGenerator('test-node');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('next()', () => {
|
|
16
|
+
it('should generate unique IDs', () => {
|
|
17
|
+
const id1 = generator.next();
|
|
18
|
+
const id2 = generator.next();
|
|
19
|
+
expect(id1).not.toBe(id2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should include node prefix', () => {
|
|
23
|
+
const id = generator.next();
|
|
24
|
+
expect(id).toContain('test-node');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should be lexicographically sortable by time', () => {
|
|
28
|
+
const id1 = generator.next();
|
|
29
|
+
// Small delay to ensure different timestamp
|
|
30
|
+
const id2 = new IdGenerator('test-node').next();
|
|
31
|
+
// IDs from different times should be sortable
|
|
32
|
+
expect(typeof id1).toBe('string');
|
|
33
|
+
expect(typeof id2).toBe('string');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should increment counter for same-millisecond IDs', () => {
|
|
37
|
+
const ids = Array.from({ length: 10 }, () => generator.next());
|
|
38
|
+
const uniqueIds = new Set(ids);
|
|
39
|
+
expect(uniqueIds.size).toBe(10);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('short()', () => {
|
|
44
|
+
it('should generate shorter IDs without node prefix', () => {
|
|
45
|
+
const id = generator.short();
|
|
46
|
+
expect(id).not.toContain('test-node');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should generate unique short IDs', () => {
|
|
50
|
+
const id1 = generator.short();
|
|
51
|
+
const id2 = generator.short();
|
|
52
|
+
expect(id1).not.toBe(id2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Singleton exports', () => {
|
|
58
|
+
it('idGen should be an IdGenerator instance', () => {
|
|
59
|
+
expect(idGen).toBeInstanceOf(IdGenerator);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('generateId should return unique IDs', () => {
|
|
63
|
+
const id1 = generateId();
|
|
64
|
+
const id2 = generateId();
|
|
65
|
+
expect(id1).not.toBe(id2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('generateId should return string', () => {
|
|
69
|
+
expect(typeof generateId()).toBe('string');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monotonic ID Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates unique, lexicographically sortable IDs that are faster than UUID v4.
|
|
5
|
+
*
|
|
6
|
+
* Format: <timestamp-base36>-<counter-base36>-<nodeId>
|
|
7
|
+
* Example: "lxyz5g8-0001-7d2a"
|
|
8
|
+
*
|
|
9
|
+
* Properties:
|
|
10
|
+
* - Lexicographically sortable by time
|
|
11
|
+
* - Unique across processes (node prefix)
|
|
12
|
+
* - ~16x faster than UUID v4
|
|
13
|
+
* - Shorter (20-24 chars vs 36 chars)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class IdGenerator {
|
|
17
|
+
private counter = 0;
|
|
18
|
+
private readonly prefix: string;
|
|
19
|
+
private lastTs = 0;
|
|
20
|
+
|
|
21
|
+
constructor(nodeId?: string) {
|
|
22
|
+
// Use process ID + random suffix for uniqueness across processes
|
|
23
|
+
this.prefix = nodeId ?? `${process.pid.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a unique, monotonically increasing ID.
|
|
28
|
+
*/
|
|
29
|
+
next(): string {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
|
|
32
|
+
// Reset counter if timestamp changed
|
|
33
|
+
if (now !== this.lastTs) {
|
|
34
|
+
this.lastTs = now;
|
|
35
|
+
this.counter = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ts = now.toString(36);
|
|
39
|
+
const seq = (this.counter++).toString(36).padStart(4, '0');
|
|
40
|
+
return `${ts}-${seq}-${this.prefix}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a short ID (just timestamp + counter, no node prefix).
|
|
45
|
+
* Use when you don't need cross-process uniqueness.
|
|
46
|
+
*/
|
|
47
|
+
short(): string {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
if (now !== this.lastTs) {
|
|
51
|
+
this.lastTs = now;
|
|
52
|
+
this.counter = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ts = now.toString(36);
|
|
56
|
+
const seq = (this.counter++).toString(36).padStart(4, '0');
|
|
57
|
+
return `${ts}-${seq}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Singleton instance for the process
|
|
62
|
+
export const idGen = new IdGenerator();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a unique ID (drop-in replacement for uuid()).
|
|
66
|
+
*/
|
|
67
|
+
export function generateId(): string {
|
|
68
|
+
return idGen.next();
|
|
69
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for UniversalIdleDetector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
6
|
+
import { UniversalIdleDetector, createIdleDetector } from './idle-detector.js';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
|
|
9
|
+
// Mock fs for Linux process state tests
|
|
10
|
+
vi.mock('node:fs', async () => {
|
|
11
|
+
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
default: {
|
|
15
|
+
...actual,
|
|
16
|
+
readFileSync: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
readFileSync: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('UniversalIdleDetector', () => {
|
|
23
|
+
let detector: UniversalIdleDetector;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
detector = new UniversalIdleDetector();
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('basic functionality', () => {
|
|
35
|
+
it('initializes with default config', () => {
|
|
36
|
+
expect(detector.getPid()).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets and gets PID', () => {
|
|
40
|
+
detector.setPid(12345);
|
|
41
|
+
expect(detector.getPid()).toBe(12345);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('tracks output and updates lastOutputTime', () => {
|
|
45
|
+
const before = Date.now();
|
|
46
|
+
detector.onOutput('some output');
|
|
47
|
+
const after = Date.now();
|
|
48
|
+
|
|
49
|
+
const silence = detector.getTimeSinceLastOutput();
|
|
50
|
+
expect(silence).toBeLessThanOrEqual(after - before + 10);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('bounds output buffer to prevent memory issues', () => {
|
|
54
|
+
// Fill buffer beyond limit
|
|
55
|
+
const chunk = 'x'.repeat(6000);
|
|
56
|
+
detector.onOutput(chunk);
|
|
57
|
+
detector.onOutput(chunk); // Now at 12000, should trim
|
|
58
|
+
|
|
59
|
+
// Internal buffer should be trimmed (we can only verify via behavior)
|
|
60
|
+
// Just verify it doesn't throw
|
|
61
|
+
const result = detector.checkIdle();
|
|
62
|
+
expect(result).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('resets state correctly', () => {
|
|
66
|
+
detector.onOutput('some output');
|
|
67
|
+
detector.setPid(12345);
|
|
68
|
+
detector.reset();
|
|
69
|
+
|
|
70
|
+
// PID should be preserved, buffer cleared
|
|
71
|
+
expect(detector.getPid()).toBe(12345);
|
|
72
|
+
// Silence time should be near zero after reset
|
|
73
|
+
expect(detector.getTimeSinceLastOutput()).toBeLessThan(50);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('checkIdle - output silence', () => {
|
|
78
|
+
it('returns not idle when output is recent', () => {
|
|
79
|
+
detector.onOutput('recent output');
|
|
80
|
+
|
|
81
|
+
const result = detector.checkIdle({ minSilenceMs: 500 });
|
|
82
|
+
|
|
83
|
+
expect(result.isIdle).toBe(false);
|
|
84
|
+
expect(result.signals).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns idle signal after silence period', async () => {
|
|
88
|
+
detector.onOutput('output');
|
|
89
|
+
|
|
90
|
+
// Wait for silence
|
|
91
|
+
await new Promise(r => setTimeout(r, 600));
|
|
92
|
+
|
|
93
|
+
const result = detector.checkIdle({ minSilenceMs: 500 });
|
|
94
|
+
|
|
95
|
+
expect(result.signals.length).toBeGreaterThan(0);
|
|
96
|
+
const silenceSignal = result.signals.find(s => s.source === 'output_silence');
|
|
97
|
+
expect(silenceSignal).toBeDefined();
|
|
98
|
+
expect(silenceSignal!.confidence).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('confidence scales with silence duration', async () => {
|
|
102
|
+
detector.onOutput('output');
|
|
103
|
+
|
|
104
|
+
// Short silence
|
|
105
|
+
await new Promise(r => setTimeout(r, 600));
|
|
106
|
+
const shortResult = detector.checkIdle({ minSilenceMs: 500 });
|
|
107
|
+
|
|
108
|
+
// Reset and wait longer
|
|
109
|
+
detector.onOutput('output');
|
|
110
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
111
|
+
const longResult = detector.checkIdle({ minSilenceMs: 500 });
|
|
112
|
+
|
|
113
|
+
const shortConfidence = shortResult.signals.find(s => s.source === 'output_silence')?.confidence ?? 0;
|
|
114
|
+
const longConfidence = longResult.signals.find(s => s.source === 'output_silence')?.confidence ?? 0;
|
|
115
|
+
|
|
116
|
+
expect(longConfidence).toBeGreaterThan(shortConfidence);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('checkIdle - natural ending detection', () => {
|
|
121
|
+
it('detects sentence-ending punctuation', async () => {
|
|
122
|
+
detector.onOutput('This is complete.');
|
|
123
|
+
await new Promise(r => setTimeout(r, 300));
|
|
124
|
+
|
|
125
|
+
const result = detector.checkIdle({ minSilenceMs: 200 });
|
|
126
|
+
|
|
127
|
+
const endingSignal = result.signals.find(s => s.source === 'natural_ending');
|
|
128
|
+
expect(endingSignal).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('detects shell prompt', async () => {
|
|
132
|
+
detector.onOutput('command output\n$ ');
|
|
133
|
+
await new Promise(r => setTimeout(r, 300));
|
|
134
|
+
|
|
135
|
+
const result = detector.checkIdle({ minSilenceMs: 200 });
|
|
136
|
+
|
|
137
|
+
const endingSignal = result.signals.find(s => s.source === 'natural_ending');
|
|
138
|
+
expect(endingSignal).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('detects code block closure', async () => {
|
|
142
|
+
detector.onOutput('function foo() {}\n```');
|
|
143
|
+
await new Promise(r => setTimeout(r, 300));
|
|
144
|
+
|
|
145
|
+
const result = detector.checkIdle({ minSilenceMs: 200 });
|
|
146
|
+
|
|
147
|
+
const endingSignal = result.signals.find(s => s.source === 'natural_ending');
|
|
148
|
+
expect(endingSignal).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('does not detect mid-sentence comma as natural ending', async () => {
|
|
152
|
+
detector.onOutput('First item,');
|
|
153
|
+
await new Promise(r => setTimeout(r, 300));
|
|
154
|
+
|
|
155
|
+
const result = detector.checkIdle({ minSilenceMs: 200 });
|
|
156
|
+
|
|
157
|
+
const endingSignal = result.signals.find(s => s.source === 'natural_ending');
|
|
158
|
+
expect(endingSignal).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('does not detect open bracket as natural ending', async () => {
|
|
162
|
+
detector.onOutput('function foo(');
|
|
163
|
+
await new Promise(r => setTimeout(r, 300));
|
|
164
|
+
|
|
165
|
+
const result = detector.checkIdle({ minSilenceMs: 200 });
|
|
166
|
+
|
|
167
|
+
const endingSignal = result.signals.find(s => s.source === 'natural_ending');
|
|
168
|
+
expect(endingSignal).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('checkIdle - process state (Linux)', () => {
|
|
173
|
+
const originalPlatform = process.platform;
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
// Mock Linux platform
|
|
177
|
+
Object.defineProperty(process, 'platform', {
|
|
178
|
+
value: 'linux',
|
|
179
|
+
configurable: true,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
Object.defineProperty(process, 'platform', {
|
|
185
|
+
value: originalPlatform,
|
|
186
|
+
configurable: true,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('detects process waiting for input via wchan', () => {
|
|
191
|
+
detector.setPid(12345);
|
|
192
|
+
|
|
193
|
+
// Mock /proc/12345/stat - S state (sleeping)
|
|
194
|
+
// Format: pid (comm) state ...
|
|
195
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation((path: string) => {
|
|
196
|
+
if (path === '/proc/12345/stat') {
|
|
197
|
+
return '12345 (node) S 1 12345 12345 0 -1 4194304 ...';
|
|
198
|
+
}
|
|
199
|
+
if (path === '/proc/12345/wchan') {
|
|
200
|
+
return 'n_tty_read';
|
|
201
|
+
}
|
|
202
|
+
throw new Error('File not found');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = detector.checkIdle();
|
|
206
|
+
|
|
207
|
+
const processSignal = result.signals.find(s => s.source === 'process_state');
|
|
208
|
+
expect(processSignal).toBeDefined();
|
|
209
|
+
expect(processSignal!.confidence).toBe(0.95);
|
|
210
|
+
expect(processSignal!.details).toBe('n_tty_read');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('detects running process as not idle', () => {
|
|
214
|
+
detector.setPid(12345);
|
|
215
|
+
|
|
216
|
+
// Mock running process (R state)
|
|
217
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation((path: string) => {
|
|
218
|
+
if (path === '/proc/12345/stat') {
|
|
219
|
+
return '12345 (node) R 1 12345 12345 0 -1 4194304 ...';
|
|
220
|
+
}
|
|
221
|
+
throw new Error('File not found');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = detector.checkIdle();
|
|
225
|
+
|
|
226
|
+
expect(result.isIdle).toBe(false);
|
|
227
|
+
expect(result.confidence).toBe(0.95);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('handles permission denied gracefully', () => {
|
|
231
|
+
detector.setPid(12345);
|
|
232
|
+
|
|
233
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
234
|
+
throw new Error('EACCES: permission denied');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Should not throw, just skip process state signal
|
|
238
|
+
const result = detector.checkIdle();
|
|
239
|
+
expect(result).toBeDefined();
|
|
240
|
+
const processSignal = result.signals.find(s => s.source === 'process_state');
|
|
241
|
+
expect(processSignal).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('handles process not found gracefully', () => {
|
|
245
|
+
detector.setPid(99999);
|
|
246
|
+
|
|
247
|
+
(fs.readFileSync as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
248
|
+
throw new Error('ENOENT: no such file or directory');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = detector.checkIdle();
|
|
252
|
+
expect(result).toBeDefined();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('checkIdle - confidence combination', () => {
|
|
257
|
+
it('boosts confidence when multiple signals agree', async () => {
|
|
258
|
+
detector.onOutput('Complete output.');
|
|
259
|
+
await new Promise(r => setTimeout(r, 600));
|
|
260
|
+
|
|
261
|
+
const result = detector.checkIdle({ minSilenceMs: 500 });
|
|
262
|
+
|
|
263
|
+
// Should have both output_silence and natural_ending signals
|
|
264
|
+
expect(result.signals.length).toBeGreaterThanOrEqual(2);
|
|
265
|
+
|
|
266
|
+
// Combined confidence should include boost
|
|
267
|
+
const maxIndividual = Math.max(...result.signals.map(s => s.confidence));
|
|
268
|
+
expect(result.confidence).toBeGreaterThanOrEqual(maxIndividual);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('respects confidence threshold for isIdle', () => {
|
|
272
|
+
const strictDetector = new UniversalIdleDetector({
|
|
273
|
+
confidenceThreshold: 0.9,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
strictDetector.onOutput('output');
|
|
277
|
+
|
|
278
|
+
// With high threshold, short silence shouldn't trigger idle
|
|
279
|
+
const result = strictDetector.checkIdle({ minSilenceMs: 100 });
|
|
280
|
+
expect(result.isIdle).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('waitForIdle', () => {
|
|
285
|
+
it('returns immediately if already idle', async () => {
|
|
286
|
+
detector.onOutput('complete.');
|
|
287
|
+
await new Promise(r => setTimeout(r, 600));
|
|
288
|
+
|
|
289
|
+
const start = Date.now();
|
|
290
|
+
const result = await detector.waitForIdle(5000, 100);
|
|
291
|
+
const elapsed = Date.now() - start;
|
|
292
|
+
|
|
293
|
+
expect(result.isIdle).toBe(true);
|
|
294
|
+
expect(elapsed).toBeLessThan(200); // Should return quickly
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('waits until idle', async () => {
|
|
298
|
+
// Start with recent output
|
|
299
|
+
detector.onOutput('working...');
|
|
300
|
+
|
|
301
|
+
// Simulate becoming idle after 500ms
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
// No more output = will become idle
|
|
304
|
+
}, 100);
|
|
305
|
+
|
|
306
|
+
const result = await detector.waitForIdle(2000, 100);
|
|
307
|
+
|
|
308
|
+
// After waiting, should detect idle based on silence
|
|
309
|
+
expect(result.signals.length).toBeGreaterThan(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('respects timeout', async () => {
|
|
313
|
+
// Keep generating output to prevent idle
|
|
314
|
+
const interval = setInterval(() => {
|
|
315
|
+
detector.onOutput('busy');
|
|
316
|
+
}, 50);
|
|
317
|
+
|
|
318
|
+
const start = Date.now();
|
|
319
|
+
const _result = await detector.waitForIdle(300, 50);
|
|
320
|
+
const elapsed = Date.now() - start;
|
|
321
|
+
|
|
322
|
+
clearInterval(interval);
|
|
323
|
+
|
|
324
|
+
expect(elapsed).toBeGreaterThanOrEqual(280);
|
|
325
|
+
expect(elapsed).toBeLessThan(500);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('createIdleDetector', () => {
|
|
330
|
+
it('creates detector with custom config', () => {
|
|
331
|
+
const detector = createIdleDetector({
|
|
332
|
+
minSilenceMs: 1000,
|
|
333
|
+
confidenceThreshold: 0.8,
|
|
334
|
+
}, { quiet: true });
|
|
335
|
+
|
|
336
|
+
expect(detector).toBeInstanceOf(UniversalIdleDetector);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Note: Platform warnings were removed to reduce terminal noise.
|
|
340
|
+
// The createIdleDetector function no longer logs warnings.
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('editor mode detection', () => {
|
|
344
|
+
it('detects vim INSERT mode', () => {
|
|
345
|
+
detector.onOutput('Some output\n-- INSERT --');
|
|
346
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('detects vim REPLACE mode', () => {
|
|
350
|
+
detector.onOutput('Some output\n-- REPLACE --');
|
|
351
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('detects vim VISUAL mode', () => {
|
|
355
|
+
detector.onOutput('Some output\n-- VISUAL --');
|
|
356
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('detects vim VISUAL LINE mode', () => {
|
|
360
|
+
detector.onOutput('Some output\n-- VISUAL LINE --');
|
|
361
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('detects vim VISUAL BLOCK mode', () => {
|
|
365
|
+
detector.onOutput('Some output\n-- VISUAL BLOCK --');
|
|
366
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('returns false for normal output', () => {
|
|
370
|
+
detector.onOutput('This is just regular output from Claude');
|
|
371
|
+
expect(detector.isInEditorMode()).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('returns false for empty buffer', () => {
|
|
375
|
+
expect(detector.isInEditorMode()).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('includes inEditorMode in checkIdle result', () => {
|
|
379
|
+
detector.onOutput('Some output\n-- INSERT --');
|
|
380
|
+
const result = detector.checkIdle();
|
|
381
|
+
expect(result.inEditorMode).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('includes inEditorMode=false when not in editor', () => {
|
|
385
|
+
detector.onOutput('Normal output');
|
|
386
|
+
const result = detector.checkIdle();
|
|
387
|
+
expect(result.inEditorMode).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|