@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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux Binary Resolver
|
|
3
|
+
*
|
|
4
|
+
* Locates tmux binary with fallback to bundled version.
|
|
5
|
+
* Priority:
|
|
6
|
+
* 1. System tmux (in PATH)
|
|
7
|
+
* 2. Bundled tmux within the agent-relay package (bin/tmux)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the package root directory (where agent-relay is installed)
|
|
21
|
+
* This works whether we're in dist/utils/ or src/utils/
|
|
22
|
+
*/
|
|
23
|
+
function getPackageRoot(): string {
|
|
24
|
+
// Navigate up from dist/utils or src/utils to package root
|
|
25
|
+
return path.resolve(__dirname, '..', '..');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Path where bundled tmux binary is installed (within the package) */
|
|
29
|
+
export function getBundledTmuxDir(): string {
|
|
30
|
+
return path.join(getPackageRoot(), 'bin');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getBundledTmuxPath(): string {
|
|
34
|
+
return path.join(getBundledTmuxDir(), 'tmux');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Legacy exports for backwards compatibility
|
|
38
|
+
export const BUNDLED_TMUX_DIR = getBundledTmuxDir();
|
|
39
|
+
export const BUNDLED_TMUX_PATH = getBundledTmuxPath();
|
|
40
|
+
|
|
41
|
+
/** Minimum supported tmux version */
|
|
42
|
+
export const MIN_TMUX_VERSION = '3.0';
|
|
43
|
+
|
|
44
|
+
export interface TmuxInfo {
|
|
45
|
+
/** Full path to tmux binary */
|
|
46
|
+
path: string;
|
|
47
|
+
/** Version string (e.g., "3.6a") */
|
|
48
|
+
version: string;
|
|
49
|
+
/** Whether this is the bundled version */
|
|
50
|
+
isBundled: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if tmux exists at a given path and get its version
|
|
55
|
+
*/
|
|
56
|
+
function getTmuxVersion(tmuxPath: string): string | null {
|
|
57
|
+
try {
|
|
58
|
+
const output = execSync(`"${tmuxPath}" -V`, {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
// Output format: "tmux 3.6a" or similar
|
|
63
|
+
const match = output.trim().match(/tmux\s+(\d+\.\d+\w?)/i);
|
|
64
|
+
return match ? match[1] : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find tmux in system PATH
|
|
72
|
+
*/
|
|
73
|
+
function findSystemTmux(): string | null {
|
|
74
|
+
try {
|
|
75
|
+
const output = execSync('which tmux', {
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
79
|
+
return output.trim() || null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve tmux binary path with fallback to bundled version.
|
|
87
|
+
* Returns null if tmux is not available.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveTmux(): TmuxInfo | null {
|
|
90
|
+
// 1. Check system tmux first
|
|
91
|
+
const systemPath = findSystemTmux();
|
|
92
|
+
if (systemPath) {
|
|
93
|
+
const version = getTmuxVersion(systemPath);
|
|
94
|
+
if (version) {
|
|
95
|
+
return {
|
|
96
|
+
path: systemPath,
|
|
97
|
+
version,
|
|
98
|
+
isBundled: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Check bundled tmux (within the package)
|
|
104
|
+
const bundledPath = getBundledTmuxPath();
|
|
105
|
+
if (fs.existsSync(bundledPath)) {
|
|
106
|
+
const version = getTmuxVersion(bundledPath);
|
|
107
|
+
if (version) {
|
|
108
|
+
return {
|
|
109
|
+
path: bundledPath,
|
|
110
|
+
version,
|
|
111
|
+
isBundled: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the tmux command to use. Throws if tmux is not available.
|
|
121
|
+
*/
|
|
122
|
+
export function getTmuxPath(): string {
|
|
123
|
+
const info = resolveTmux();
|
|
124
|
+
if (!info) {
|
|
125
|
+
throw new TmuxNotFoundError();
|
|
126
|
+
}
|
|
127
|
+
return info.path;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if tmux is available (either system or bundled)
|
|
132
|
+
*/
|
|
133
|
+
export function isTmuxAvailable(): boolean {
|
|
134
|
+
return resolveTmux() !== null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get platform identifier for downloading binaries
|
|
139
|
+
*/
|
|
140
|
+
export function getPlatformIdentifier(): string | null {
|
|
141
|
+
const platform = os.platform();
|
|
142
|
+
const arch = os.arch();
|
|
143
|
+
|
|
144
|
+
if (platform === 'darwin') {
|
|
145
|
+
return arch === 'arm64' ? 'macos-arm64' : 'macos-x86_64';
|
|
146
|
+
} else if (platform === 'linux') {
|
|
147
|
+
return arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Unsupported platform
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Error thrown when tmux is not available
|
|
156
|
+
*/
|
|
157
|
+
export class TmuxNotFoundError extends Error {
|
|
158
|
+
constructor() {
|
|
159
|
+
const platformInstructions = (() => {
|
|
160
|
+
switch (os.platform()) {
|
|
161
|
+
case 'darwin':
|
|
162
|
+
return ' macOS: brew install tmux';
|
|
163
|
+
case 'linux':
|
|
164
|
+
return ' Ubuntu/Debian: sudo apt install tmux\n Fedora: sudo dnf install tmux\n Arch: sudo pacman -S tmux';
|
|
165
|
+
case 'win32':
|
|
166
|
+
return ' Windows: tmux requires WSL (Windows Subsystem for Linux)\n Install WSL, then: sudo apt install tmux';
|
|
167
|
+
default:
|
|
168
|
+
return ' See: https://github.com/tmux/tmux/wiki/Installing';
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
|
|
172
|
+
super(
|
|
173
|
+
`tmux is required but not found.\n\nInstall tmux:\n${platformInstructions}\n\nThen reinstall agent-relay: npm install agent-relay`
|
|
174
|
+
);
|
|
175
|
+
this.name = 'TmuxNotFoundError';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse version string to compare versions
|
|
181
|
+
*/
|
|
182
|
+
function parseVersion(version: string): { major: number; minor: number } {
|
|
183
|
+
const match = version.match(/(\d+)\.(\d+)/);
|
|
184
|
+
if (!match) {
|
|
185
|
+
return { major: 0, minor: 0 };
|
|
186
|
+
}
|
|
187
|
+
return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if installed tmux version meets minimum requirements
|
|
192
|
+
*/
|
|
193
|
+
export function checkTmuxVersion(): { ok: boolean; version: string | null; minimum: string } {
|
|
194
|
+
const info = resolveTmux();
|
|
195
|
+
if (!info) {
|
|
196
|
+
return { ok: false, version: null, minimum: MIN_TMUX_VERSION };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const installed = parseVersion(info.version);
|
|
200
|
+
const required = parseVersion(MIN_TMUX_VERSION);
|
|
201
|
+
|
|
202
|
+
const ok =
|
|
203
|
+
installed.major > required.major ||
|
|
204
|
+
(installed.major === required.major && installed.minor >= required.minor);
|
|
205
|
+
|
|
206
|
+
return { ok, version: info.version, minimum: MIN_TMUX_VERSION };
|
|
207
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for TmuxWrapper constants and utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { getDefaultPrefix } from './tmux-wrapper.js';
|
|
7
|
+
import {
|
|
8
|
+
type InjectionResult,
|
|
9
|
+
INJECTION_CONSTANTS,
|
|
10
|
+
createInjectionMetrics,
|
|
11
|
+
} from './shared.js';
|
|
12
|
+
|
|
13
|
+
describe('TmuxWrapper constants', () => {
|
|
14
|
+
// Unified prefix across all CLI types
|
|
15
|
+
describe('getDefaultPrefix', () => {
|
|
16
|
+
it('returns ->relay: for gemini CLI type', () => {
|
|
17
|
+
expect(getDefaultPrefix('gemini')).toBe('->relay:');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns ->relay: for claude CLI type', () => {
|
|
21
|
+
expect(getDefaultPrefix('claude')).toBe('->relay:');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns ->relay: for codex CLI type', () => {
|
|
25
|
+
expect(getDefaultPrefix('codex')).toBe('->relay:');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns ->relay: for other CLI type', () => {
|
|
29
|
+
expect(getDefaultPrefix('other')).toBe('->relay:');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('String truncation safety', () => {
|
|
35
|
+
// Test the truncation pattern used throughout tmux-wrapper
|
|
36
|
+
// Pattern: str.substring(0, Math.min(LIMIT, str.length))
|
|
37
|
+
|
|
38
|
+
const safeSubstring = (str: string, maxLen: number): string => {
|
|
39
|
+
return str.substring(0, Math.min(maxLen, str.length));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('safeSubstring helper pattern', () => {
|
|
43
|
+
it('truncates long strings', () => {
|
|
44
|
+
const longString = 'a'.repeat(100);
|
|
45
|
+
expect(safeSubstring(longString, 40)).toBe('a'.repeat(40));
|
|
46
|
+
expect(safeSubstring(longString, 40)).toHaveLength(40);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves short strings', () => {
|
|
50
|
+
const shortString = 'hello';
|
|
51
|
+
expect(safeSubstring(shortString, 40)).toBe('hello');
|
|
52
|
+
expect(safeSubstring(shortString, 40)).toHaveLength(5);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles exact length strings', () => {
|
|
56
|
+
const exactString = 'a'.repeat(40);
|
|
57
|
+
expect(safeSubstring(exactString, 40)).toBe(exactString);
|
|
58
|
+
expect(safeSubstring(exactString, 40)).toHaveLength(40);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles empty strings', () => {
|
|
62
|
+
expect(safeSubstring('', 40)).toBe('');
|
|
63
|
+
expect(safeSubstring('', 40)).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles strings shorter than limit', () => {
|
|
67
|
+
expect(safeSubstring('ab', 40)).toBe('ab');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('handles limit of 0', () => {
|
|
71
|
+
expect(safeSubstring('hello', 0)).toBe('');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles unicode characters', () => {
|
|
75
|
+
const unicodeStr = ''.repeat(100);
|
|
76
|
+
expect(safeSubstring(unicodeStr, 10)).toBe(''.repeat(10));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('DEBUG_LOG_TRUNCATE_LENGTH constant (40)', () => {
|
|
81
|
+
const DEBUG_LOG_TRUNCATE_LENGTH = 40;
|
|
82
|
+
|
|
83
|
+
it('truncates debug log content appropriately', () => {
|
|
84
|
+
const longMessage = 'This is a very long debug message that exceeds the limit';
|
|
85
|
+
const truncated = safeSubstring(longMessage, DEBUG_LOG_TRUNCATE_LENGTH);
|
|
86
|
+
expect(truncated).toBe('This is a very long debug message that e');
|
|
87
|
+
expect(truncated).toHaveLength(40);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('RELAY_LOG_TRUNCATE_LENGTH constant (50)', () => {
|
|
92
|
+
const RELAY_LOG_TRUNCATE_LENGTH = 50;
|
|
93
|
+
|
|
94
|
+
it('truncates relay command log content appropriately', () => {
|
|
95
|
+
const longMessage = 'This is a very long relay message that definitely exceeds the fifty character limit';
|
|
96
|
+
const truncated = safeSubstring(longMessage, RELAY_LOG_TRUNCATE_LENGTH);
|
|
97
|
+
expect(truncated).toBe('This is a very long relay message that definitely ');
|
|
98
|
+
expect(truncated).toHaveLength(50);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Cursor stability constants', () => {
|
|
104
|
+
// These test the logic that uses STABLE_CURSOR_THRESHOLD and MAX_PROMPT_CURSOR_POSITION
|
|
105
|
+
|
|
106
|
+
const STABLE_CURSOR_THRESHOLD = 3;
|
|
107
|
+
const MAX_PROMPT_CURSOR_POSITION = 4;
|
|
108
|
+
|
|
109
|
+
describe('STABLE_CURSOR_THRESHOLD', () => {
|
|
110
|
+
it('requires 3 or more stable polls to consider input clear', () => {
|
|
111
|
+
// Simulate cursor stability counting
|
|
112
|
+
let stableCursorCount = 0;
|
|
113
|
+
const cursorX = 2;
|
|
114
|
+
|
|
115
|
+
// First poll - not stable yet
|
|
116
|
+
stableCursorCount++;
|
|
117
|
+
expect(stableCursorCount >= STABLE_CURSOR_THRESHOLD).toBe(false);
|
|
118
|
+
|
|
119
|
+
// Second poll - still not stable
|
|
120
|
+
stableCursorCount++;
|
|
121
|
+
expect(stableCursorCount >= STABLE_CURSOR_THRESHOLD).toBe(false);
|
|
122
|
+
|
|
123
|
+
// Third poll - now stable
|
|
124
|
+
stableCursorCount++;
|
|
125
|
+
expect(stableCursorCount >= STABLE_CURSOR_THRESHOLD).toBe(true);
|
|
126
|
+
expect(cursorX <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('resets count when cursor moves', () => {
|
|
130
|
+
let stableCursorCount = 2;
|
|
131
|
+
let lastCursorX = 2;
|
|
132
|
+
const newCursorX = 5; // Cursor moved
|
|
133
|
+
|
|
134
|
+
if (newCursorX !== lastCursorX) {
|
|
135
|
+
stableCursorCount = 0;
|
|
136
|
+
lastCursorX = newCursorX;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expect(stableCursorCount).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('MAX_PROMPT_CURSOR_POSITION', () => {
|
|
144
|
+
it('considers positions 0-4 as typical prompt positions', () => {
|
|
145
|
+
expect(0 <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
146
|
+
expect(1 <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
147
|
+
expect(2 <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
148
|
+
expect(3 <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
149
|
+
expect(4 <= MAX_PROMPT_CURSOR_POSITION).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('considers positions > 4 as likely having user input', () => {
|
|
153
|
+
expect(5 <= MAX_PROMPT_CURSOR_POSITION).toBe(false);
|
|
154
|
+
expect(10 <= MAX_PROMPT_CURSOR_POSITION).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('works with combined stability check', () => {
|
|
158
|
+
const stableCursorCount = 3;
|
|
159
|
+
const cursorAtPrompt = 2;
|
|
160
|
+
const cursorWithInput = 10;
|
|
161
|
+
|
|
162
|
+
// At prompt position - should be considered clear
|
|
163
|
+
const isClearAtPrompt =
|
|
164
|
+
stableCursorCount >= STABLE_CURSOR_THRESHOLD &&
|
|
165
|
+
cursorAtPrompt <= MAX_PROMPT_CURSOR_POSITION;
|
|
166
|
+
expect(isClearAtPrompt).toBe(true);
|
|
167
|
+
|
|
168
|
+
// With input - should not be considered clear
|
|
169
|
+
const isClearWithInput =
|
|
170
|
+
stableCursorCount >= STABLE_CURSOR_THRESHOLD &&
|
|
171
|
+
cursorWithInput <= MAX_PROMPT_CURSOR_POSITION;
|
|
172
|
+
expect(isClearWithInput).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Injection retry logic', () => {
|
|
178
|
+
// Test the retry logic pattern used by injectWithRetry
|
|
179
|
+
|
|
180
|
+
describe('INJECTION_CONSTANTS', () => {
|
|
181
|
+
it('has correct MAX_RETRIES', () => {
|
|
182
|
+
expect(INJECTION_CONSTANTS.MAX_RETRIES).toBe(3);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('has correct VERIFICATION_TIMEOUT_MS', () => {
|
|
186
|
+
expect(INJECTION_CONSTANTS.VERIFICATION_TIMEOUT_MS).toBe(2000);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('has correct RETRY_BACKOFF_MS', () => {
|
|
190
|
+
expect(INJECTION_CONSTANTS.RETRY_BACKOFF_MS).toBe(200);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('InjectionMetrics tracking', () => {
|
|
195
|
+
it('initializes with zero counts', () => {
|
|
196
|
+
const metrics = createInjectionMetrics();
|
|
197
|
+
expect(metrics.total).toBe(0);
|
|
198
|
+
expect(metrics.successFirstTry).toBe(0);
|
|
199
|
+
expect(metrics.successWithRetry).toBe(0);
|
|
200
|
+
expect(metrics.failed).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('tracks successful first-try injection', () => {
|
|
204
|
+
const metrics = createInjectionMetrics();
|
|
205
|
+
|
|
206
|
+
// Simulate successful first-try injection
|
|
207
|
+
metrics.total++;
|
|
208
|
+
const verified = true;
|
|
209
|
+
const attempt = 0;
|
|
210
|
+
if (verified && attempt === 0) {
|
|
211
|
+
metrics.successFirstTry++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
expect(metrics.total).toBe(1);
|
|
215
|
+
expect(metrics.successFirstTry).toBe(1);
|
|
216
|
+
expect(metrics.successWithRetry).toBe(0);
|
|
217
|
+
expect(metrics.failed).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('tracks successful retry injection', () => {
|
|
221
|
+
const metrics = createInjectionMetrics();
|
|
222
|
+
|
|
223
|
+
// Simulate successful injection on retry
|
|
224
|
+
metrics.total++;
|
|
225
|
+
const verified = true;
|
|
226
|
+
const attempt = 2; // Third attempt
|
|
227
|
+
if (verified && attempt > 0) {
|
|
228
|
+
metrics.successWithRetry++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(metrics.total).toBe(1);
|
|
232
|
+
expect(metrics.successFirstTry).toBe(0);
|
|
233
|
+
expect(metrics.successWithRetry).toBe(1);
|
|
234
|
+
expect(metrics.failed).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('tracks failed injection', () => {
|
|
238
|
+
const metrics = createInjectionMetrics();
|
|
239
|
+
|
|
240
|
+
// Simulate failed injection after all retries
|
|
241
|
+
metrics.total++;
|
|
242
|
+
metrics.failed++;
|
|
243
|
+
|
|
244
|
+
expect(metrics.total).toBe(1);
|
|
245
|
+
expect(metrics.successFirstTry).toBe(0);
|
|
246
|
+
expect(metrics.successWithRetry).toBe(0);
|
|
247
|
+
expect(metrics.failed).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('InjectionResult structure', () => {
|
|
252
|
+
it('returns success result on first try', () => {
|
|
253
|
+
const result: InjectionResult = { success: true, attempts: 1 };
|
|
254
|
+
expect(result.success).toBe(true);
|
|
255
|
+
expect(result.attempts).toBe(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('returns success result after retries', () => {
|
|
259
|
+
const result: InjectionResult = { success: true, attempts: 3 };
|
|
260
|
+
expect(result.success).toBe(true);
|
|
261
|
+
expect(result.attempts).toBe(3);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns failure result after max retries', () => {
|
|
265
|
+
const result: InjectionResult = {
|
|
266
|
+
success: false,
|
|
267
|
+
attempts: INJECTION_CONSTANTS.MAX_RETRIES,
|
|
268
|
+
};
|
|
269
|
+
expect(result.success).toBe(false);
|
|
270
|
+
expect(result.attempts).toBe(3);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('can include fallback flag', () => {
|
|
274
|
+
const result: InjectionResult = {
|
|
275
|
+
success: false,
|
|
276
|
+
attempts: 3,
|
|
277
|
+
fallbackUsed: true,
|
|
278
|
+
};
|
|
279
|
+
expect(result.fallbackUsed).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('Backoff calculation', () => {
|
|
284
|
+
it('increases backoff with each attempt', () => {
|
|
285
|
+
const backoffMs = INJECTION_CONSTANTS.RETRY_BACKOFF_MS;
|
|
286
|
+
|
|
287
|
+
// Backoff for attempt 0 (first retry)
|
|
288
|
+
const backoff0 = backoffMs * 1;
|
|
289
|
+
expect(backoff0).toBe(200);
|
|
290
|
+
|
|
291
|
+
// Backoff for attempt 1 (second retry)
|
|
292
|
+
const backoff1 = backoffMs * 2;
|
|
293
|
+
expect(backoff1).toBe(400);
|
|
294
|
+
|
|
295
|
+
// Backoff for attempt 2 (third retry - but no backoff needed after last attempt)
|
|
296
|
+
const backoff2 = backoffMs * 3;
|
|
297
|
+
expect(backoff2).toBe(600);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('Verification pattern matching', () => {
|
|
302
|
+
it('generates correct expected pattern', () => {
|
|
303
|
+
const shortId = 'abc12345';
|
|
304
|
+
const from = 'TestAgent';
|
|
305
|
+
const expectedPattern = `Relay message from ${from} [${shortId}]`;
|
|
306
|
+
expect(expectedPattern).toBe('Relay message from TestAgent [abc12345]');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('handles different agent names', () => {
|
|
310
|
+
const shortId = 'def67890';
|
|
311
|
+
const from = 'Backend';
|
|
312
|
+
const expectedPattern = `Relay message from ${from} [${shortId}]`;
|
|
313
|
+
expect(expectedPattern).toBe('Relay message from Backend [def67890]');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|