vibecodingmachine-cli 2026.1.23-1010 → 2026.1.29-1432
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/package.json +14 -5
- package/src/utils/interactive.js +53 -3
- package/.allnightai/REQUIREMENTS.md +0 -11
- package/.eslintrc.js +0 -16
- package/__tests__/antigravity-js-handler.test.js +0 -23
- package/__tests__/provider-manager.test.js +0 -84
- package/__tests__/provider-rate-cache.test.js +0 -27
- package/jest.config.js +0 -8
- package/logs/audit/2025-11-07.jsonl +0 -2
- package/logs/audit/2025-12-24.jsonl +0 -2
- package/logs/audit/2025-12-27.jsonl +0 -1
- package/logs/audit/2026-01-03.jsonl +0 -2
- package/repro_open.js +0 -13
- package/reproduce_issue.js +0 -160
- package/reset_provider_order.js +0 -21
- package/scripts/README.md +0 -128
- package/scripts/auto-start-wrapper.sh +0 -92
- package/scripts/convert-requirements.js +0 -35
- package/scripts/debug-parse.js +0 -24
- package/src/commands/auto.js.bak +0 -710
- package/src/utils/auto-mode-ui.js.bak.blessed +0 -207
- package/tests/antigravity-js-handler.test.js +0 -23
- package/tests/auto-mode.test.js +0 -37
- package/tests/config.test.js +0 -34
- package/tests/home-bootstrap.test.js +0 -76
- package/tests/integration/health-tracking.integration.test.js +0 -284
- package/tests/provider-manager.test.js +0 -92
- package/tests/rate-limit-display.test.js +0 -44
- package/tests/requirements-bullet-parsing.test.js +0 -15
- package/tests/requirements-converter.test.js +0 -42
- package/tests/requirements-heading-count.test.js +0 -27
- package/tests/requirements-legacy-parsing.test.js +0 -15
- package/tests/requirements-navigator-buildtree-await.test.js +0 -28
- package/tests/requirements-parse-integration.test.js +0 -44
- package/tests/wait-for-ide-completion.test.js +0 -56
- package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +0 -61
- package/tests/wait-for-ide-quota-detection-cursor.test.js +0 -60
- package/tests/wait-for-ide-quota-detection-negative.test.js +0 -45
- package/tests/wait-for-ide-quota-detection.test.js +0 -59
- package/verify_fix.js +0 -36
- package/verify_ui.js +0 -38
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
const blessed = require('blessed');
|
|
2
|
-
const chalk = require('chalk');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Create and display a blessed-based UI for Auto Mode
|
|
6
|
-
* Shows: persistent menu header, status card, and scrolling output
|
|
7
|
-
* @param {object} options - UI configuration
|
|
8
|
-
* @param {string} options.menuContent - Menu content to display in header
|
|
9
|
-
* @param {function} options.onExit - Callback when user presses Ctrl+C or q
|
|
10
|
-
* @returns {object} UI interface with methods to update status and append output
|
|
11
|
-
*/
|
|
12
|
-
function createAutoModeUI(options = {}) {
|
|
13
|
-
const { menuContent = '', onExit } = options;
|
|
14
|
-
|
|
15
|
-
// Create screen
|
|
16
|
-
const screen = blessed.screen({
|
|
17
|
-
smartCSR: true,
|
|
18
|
-
title: 'AllNightAI Auto Mode',
|
|
19
|
-
fullUnicode: true,
|
|
20
|
-
forceUnicode: true
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Clear screen and render immediately
|
|
24
|
-
screen.clearRegion(0, screen.width, 0, screen.height);
|
|
25
|
-
|
|
26
|
-
// Header box (menu) - use fixed height instead of shrink
|
|
27
|
-
const headerHeight = 11; // Fixed height for header
|
|
28
|
-
const header = blessed.box({
|
|
29
|
-
top: 0,
|
|
30
|
-
left: 0,
|
|
31
|
-
width: '100%',
|
|
32
|
-
height: headerHeight,
|
|
33
|
-
content: menuContent,
|
|
34
|
-
tags: true,
|
|
35
|
-
border: {
|
|
36
|
-
type: 'line'
|
|
37
|
-
},
|
|
38
|
-
style: {
|
|
39
|
-
border: {
|
|
40
|
-
fg: 'cyan'
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Status card (purple/magenta border like GUI)
|
|
46
|
-
const statusCardHeight = 12;
|
|
47
|
-
const statusCard = blessed.box({
|
|
48
|
-
top: headerHeight,
|
|
49
|
-
left: 0,
|
|
50
|
-
width: '100%',
|
|
51
|
-
height: statusCardHeight,
|
|
52
|
-
tags: true,
|
|
53
|
-
border: {
|
|
54
|
-
type: 'round'
|
|
55
|
-
},
|
|
56
|
-
style: {
|
|
57
|
-
border: {
|
|
58
|
-
fg: 'magenta'
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
label: ' Auto Mode Status '
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Output log (scrollable)
|
|
65
|
-
const totalHeaderHeight = headerHeight + statusCardHeight;
|
|
66
|
-
const outputLog = blessed.log({
|
|
67
|
-
top: totalHeaderHeight,
|
|
68
|
-
left: 0,
|
|
69
|
-
width: '100%',
|
|
70
|
-
height: `100%-${totalHeaderHeight}`,
|
|
71
|
-
tags: true,
|
|
72
|
-
border: {
|
|
73
|
-
type: 'line'
|
|
74
|
-
},
|
|
75
|
-
style: {
|
|
76
|
-
border: {
|
|
77
|
-
fg: 'green'
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
label: ' Aider Output ',
|
|
81
|
-
scrollable: true,
|
|
82
|
-
alwaysScroll: true,
|
|
83
|
-
mouse: true,
|
|
84
|
-
keys: true,
|
|
85
|
-
vi: true,
|
|
86
|
-
scrollbar: {
|
|
87
|
-
ch: ' ',
|
|
88
|
-
track: {
|
|
89
|
-
bg: 'cyan'
|
|
90
|
-
},
|
|
91
|
-
style: {
|
|
92
|
-
inverse: true
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Append all elements to screen
|
|
98
|
-
screen.append(header);
|
|
99
|
-
screen.append(statusCard);
|
|
100
|
-
screen.append(outputLog);
|
|
101
|
-
|
|
102
|
-
// Focus the output log for scrolling
|
|
103
|
-
outputLog.focus();
|
|
104
|
-
|
|
105
|
-
// Key bindings
|
|
106
|
-
screen.key(['escape', 'q', 'C-c'], () => {
|
|
107
|
-
if (onExit) {
|
|
108
|
-
onExit();
|
|
109
|
-
}
|
|
110
|
-
screen.destroy();
|
|
111
|
-
process.exit(0);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Initial render
|
|
115
|
-
screen.render();
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Update the status card with new status information
|
|
119
|
-
* @param {object} status - Status object
|
|
120
|
-
* @param {string} status.requirement - Current requirement
|
|
121
|
-
* @param {string} status.step - Current step (PREPARE, ACT, CLEAN UP, VERIFY, DONE)
|
|
122
|
-
* @param {number} status.chatCount - Current chat count
|
|
123
|
-
* @param {number|null} status.maxChats - Max chats or null for unlimited
|
|
124
|
-
* @param {number} status.progress - Progress percentage (0-100)
|
|
125
|
-
*/
|
|
126
|
-
function updateStatus(status) {
|
|
127
|
-
const {
|
|
128
|
-
requirement = 'No requirement loaded',
|
|
129
|
-
step = 'UNKNOWN',
|
|
130
|
-
chatCount = 0,
|
|
131
|
-
maxChats = null,
|
|
132
|
-
progress = 0
|
|
133
|
-
} = status;
|
|
134
|
-
|
|
135
|
-
// Step color mapping
|
|
136
|
-
const stepColors = {
|
|
137
|
-
'PREPARE': '{cyan-fg}',
|
|
138
|
-
'ACT': '{yellow-fg}',
|
|
139
|
-
'CLEAN UP': '{magenta-fg}',
|
|
140
|
-
'VERIFY': '{blue-fg}',
|
|
141
|
-
'DONE': '{green-fg}',
|
|
142
|
-
'UNKNOWN': '{gray-fg}'
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const stepColor = stepColors[step] || '{gray-fg}';
|
|
146
|
-
|
|
147
|
-
// Progress bar
|
|
148
|
-
const barWidth = 40;
|
|
149
|
-
const filledWidth = Math.round((progress / 100) * barWidth);
|
|
150
|
-
const emptyWidth = barWidth - filledWidth;
|
|
151
|
-
const progressBar = '{green-fg}' + '█'.repeat(filledWidth) + '{/green-fg}' +
|
|
152
|
-
'{gray-fg}' + '░'.repeat(emptyWidth) + '{/gray-fg}';
|
|
153
|
-
|
|
154
|
-
// Chat counter
|
|
155
|
-
const chatDisplay = maxChats
|
|
156
|
-
? `Chat ${chatCount}/${maxChats}`
|
|
157
|
-
: `Chat ${chatCount} (unlimited)`;
|
|
158
|
-
|
|
159
|
-
// Build card content
|
|
160
|
-
const content = `
|
|
161
|
-
{bold}📋 Current Requirement{/bold}
|
|
162
|
-
|
|
163
|
-
${requirement.length > 70 ? requirement.substring(0, 67) + '...' : requirement}
|
|
164
|
-
|
|
165
|
-
{bold}🚦 Status:{/bold} ${stepColor}{bold}${step}{/bold}{/}
|
|
166
|
-
|
|
167
|
-
${progressBar} ${progress}%
|
|
168
|
-
|
|
169
|
-
{gray-fg}${chatDisplay}{/gray-fg}
|
|
170
|
-
`;
|
|
171
|
-
|
|
172
|
-
statusCard.setContent(content);
|
|
173
|
-
screen.render();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Append a line to the output log
|
|
178
|
-
* @param {string} line - Line to append
|
|
179
|
-
*/
|
|
180
|
-
function appendOutput(line) {
|
|
181
|
-
outputLog.log(line);
|
|
182
|
-
screen.render();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Clear the output log
|
|
187
|
-
*/
|
|
188
|
-
function clearOutput() {
|
|
189
|
-
outputLog.setContent('');
|
|
190
|
-
screen.render();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
screen,
|
|
195
|
-
header,
|
|
196
|
-
statusCard,
|
|
197
|
-
outputLog,
|
|
198
|
-
updateStatus,
|
|
199
|
-
appendOutput,
|
|
200
|
-
clearOutput,
|
|
201
|
-
destroy: () => screen.destroy()
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
module.exports = {
|
|
206
|
-
createAutoModeUI
|
|
207
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
const { handleAntigravityRateLimit } = require('../src/utils/antigravity-js-handler');
|
|
2
|
-
const providerRegistry = require('../src/utils/provider-registry');
|
|
3
|
-
|
|
4
|
-
jest.mock('../src/utils/provider-registry');
|
|
5
|
-
|
|
6
|
-
describe('handleAntigravityRateLimit', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
jest.resetAllMocks();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('suggests next provider and does not persistently disable antigravity', async () => {
|
|
12
|
-
providerRegistry.getProviderPreferences.mockResolvedValue({
|
|
13
|
-
order: ['antigravity', 'vscode'],
|
|
14
|
-
enabled: { antigravity: true, vscode: true }
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const result = await handleAntigravityRateLimit();
|
|
18
|
-
|
|
19
|
-
expect(result.success).toBe(true);
|
|
20
|
-
expect(result.nextProvider).toBe('vscode');
|
|
21
|
-
expect(providerRegistry.saveProviderPreferences).not.toHaveBeenCalled();
|
|
22
|
-
});
|
|
23
|
-
});
|
package/tests/auto-mode.test.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
|
-
|
|
5
|
-
describe('auto-mode utils', () => {
|
|
6
|
-
const tmpConfig = path.join(os.tmpdir(), `vibecodingmachine_test_config_${Date.now()}.json`);
|
|
7
|
-
const tmpRepo = path.join(os.tmpdir(), `vibecodingmachine_test_repo_${Date.now()}`);
|
|
8
|
-
|
|
9
|
-
beforeAll(async () => {
|
|
10
|
-
process.env.VIBECODINGMACHINE_CONFIG_PATH = tmpConfig;
|
|
11
|
-
await fs.ensureDir(path.join(tmpRepo, '.vibecodingmachine', 'temp'));
|
|
12
|
-
const { setRepoPath } = require('../src/utils/config');
|
|
13
|
-
await setRepoPath(tmpRepo);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
delete process.env.VIBECODINGMACHINE_CONFIG_PATH;
|
|
18
|
-
await fs.remove(tmpConfig).catch(() => {});
|
|
19
|
-
await fs.remove(tmpRepo).catch(() => {});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('start/stop and status', async () => {
|
|
23
|
-
const { startAutoMode, stopAutoMode, checkAutoModeStatus } = require('../src/utils/auto-mode');
|
|
24
|
-
const { getAutoConfig } = require('../src/utils/config');
|
|
25
|
-
await startAutoMode(tmpRepo, { ide: 'cursor' });
|
|
26
|
-
let status = await checkAutoModeStatus();
|
|
27
|
-
expect(status.running).toBe(true);
|
|
28
|
-
expect(status.ide).toBe('cursor');
|
|
29
|
-
|
|
30
|
-
await stopAutoMode();
|
|
31
|
-
status = await checkAutoModeStatus();
|
|
32
|
-
expect(status.running).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
package/tests/config.test.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
|
-
|
|
5
|
-
describe('config utils', () => {
|
|
6
|
-
const tmpConfig = path.join(os.tmpdir(), `vibecodingmachine_test_config_${Date.now()}.json`);
|
|
7
|
-
|
|
8
|
-
beforeAll(() => {
|
|
9
|
-
process.env.VIBECODINGMACHINE_CONFIG_PATH = tmpConfig;
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
afterAll(async () => {
|
|
13
|
-
delete process.env.VIBECODINGMACHINE_CONFIG_PATH;
|
|
14
|
-
await fs.remove(tmpConfig).catch(() => {});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('set/get repo path roundtrip', async () => {
|
|
18
|
-
const { getRepoPath, setRepoPath } = require('../src/utils/config');
|
|
19
|
-
expect(await getRepoPath()).toBeNull();
|
|
20
|
-
await setRepoPath('/tmp/repo');
|
|
21
|
-
expect(await getRepoPath()).toBe('/tmp/repo');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('set/get auto config roundtrip', async () => {
|
|
25
|
-
const { getAutoConfig, setAutoConfig } = require('../src/utils/config');
|
|
26
|
-
await setAutoConfig({ ide: 'cursor', maxChats: 10 });
|
|
27
|
-
const cfg = await getAutoConfig();
|
|
28
|
-
expect(cfg.ide).toBe('cursor');
|
|
29
|
-
expect(cfg.maxChats).toBe(10);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
|
-
|
|
5
|
-
describe('interactive home directory bootstrap', () => {
|
|
6
|
-
const tmpHome = path.join(os.tmpdir(), `vibecodingmachine_test_home_${Date.now()}`);
|
|
7
|
-
const tmpConfig = path.join(os.tmpdir(), `vibecodingmachine_test_config_${Date.now()}.json`);
|
|
8
|
-
|
|
9
|
-
beforeAll(async () => {
|
|
10
|
-
process.env.VIBECODINGMACHINE_CONFIG_PATH = tmpConfig;
|
|
11
|
-
await fs.ensureDir(tmpHome);
|
|
12
|
-
|
|
13
|
-
jest.resetModules();
|
|
14
|
-
|
|
15
|
-
jest.spyOn(os, 'homedir').mockReturnValue(tmpHome);
|
|
16
|
-
|
|
17
|
-
process.chdir(tmpHome);
|
|
18
|
-
|
|
19
|
-
const prompts = [];
|
|
20
|
-
jest.doMock('inquirer', () => ({
|
|
21
|
-
prompt: async (questions) => {
|
|
22
|
-
prompts.push(questions);
|
|
23
|
-
const q0 = Array.isArray(questions) ? questions[0] : questions;
|
|
24
|
-
|
|
25
|
-
if (q0 && q0.name === 'shouldCreateCodeDir') {
|
|
26
|
-
return { shouldCreateCodeDir: true };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (q0 && q0.name === 'projectName') {
|
|
30
|
-
return { projectName: 'My Project Name' };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
jest.doMock('vibecodingmachine-core', () => ({
|
|
38
|
-
checkVibeCodingMachineExists: async () => ({ insideExists: false, siblingExists: false }),
|
|
39
|
-
getHostname: () => 'test-host',
|
|
40
|
-
getRequirementsFilename: async () => 'REQUIREMENTS.md',
|
|
41
|
-
requirementsExists: async () => false,
|
|
42
|
-
isComputerNameEnabled: async () => false,
|
|
43
|
-
t: (k) => k,
|
|
44
|
-
detectLocale: () => 'en',
|
|
45
|
-
setLocale: () => {}
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
afterAll(async () => {
|
|
52
|
-
jest.restoreAllMocks();
|
|
53
|
-
delete process.env.VIBECODINGMACHINE_CONFIG_PATH;
|
|
54
|
-
await fs.remove(tmpConfig).catch(() => {});
|
|
55
|
-
await fs.remove(tmpHome).catch(() => {});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('bootstraps ~/code/<project> from home and can init .vibecodingmachine inside it', async () => {
|
|
59
|
-
const { bootstrapProjectIfInHomeDir } = require('../src/utils/interactive');
|
|
60
|
-
await bootstrapProjectIfInHomeDir();
|
|
61
|
-
|
|
62
|
-
const projectDir = path.join(tmpHome, 'code', 'my-project-name');
|
|
63
|
-
const projectDirReal = await fs.realpath(projectDir);
|
|
64
|
-
expect(await fs.realpath(process.cwd())).toBe(projectDirReal);
|
|
65
|
-
expect(await fs.pathExists(projectDirReal)).toBe(true);
|
|
66
|
-
|
|
67
|
-
const { initRepo } = require('../src/commands/repo');
|
|
68
|
-
await initRepo({ location: 'inside' });
|
|
69
|
-
|
|
70
|
-
const vcmDir = path.join(projectDirReal, '.vibecodingmachine');
|
|
71
|
-
expect(await fs.pathExists(vcmDir)).toBe(true);
|
|
72
|
-
|
|
73
|
-
const requirementsPath = path.join(vcmDir, 'REQUIREMENTS.md');
|
|
74
|
-
expect(await fs.pathExists(requirementsPath)).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for Health Tracking
|
|
3
|
-
* Tests cross-component behavior and data persistence
|
|
4
|
-
* @jest-environment node
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs-extra');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const { IDEHealthTracker } = require('vibecodingmachine-core');
|
|
11
|
-
|
|
12
|
-
describe('Health Tracking Integration', () => {
|
|
13
|
-
let testStorageFile;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
testStorageFile = path.join(os.tmpdir(), `integration-test-health-${Date.now()}.json`);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
if (await fs.pathExists(testStorageFile)) {
|
|
21
|
-
await fs.remove(testStorageFile);
|
|
22
|
-
}
|
|
23
|
-
const backupFile = `${testStorageFile}.bak`;
|
|
24
|
-
if (await fs.pathExists(backupFile)) {
|
|
25
|
-
await fs.remove(backupFile);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('Data Persistence', () => {
|
|
30
|
-
it('should persist health data across tracker instances', async () => {
|
|
31
|
-
// Create first tracker and record interactions
|
|
32
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
33
|
-
|
|
34
|
-
await tracker1.recordSuccess('cursor', 120000, {
|
|
35
|
-
requirementId: 'req-001',
|
|
36
|
-
continuationPromptsDetected: 1,
|
|
37
|
-
});
|
|
38
|
-
await tracker1.recordSuccess('cursor', 115000);
|
|
39
|
-
await tracker1.recordFailure('windsurf', 'Timeout exceeded', {
|
|
40
|
-
timeoutUsed: 1800000,
|
|
41
|
-
});
|
|
42
|
-
await tracker1.recordQuota('vscode', 'Monthly quota exceeded');
|
|
43
|
-
|
|
44
|
-
// Save explicitly
|
|
45
|
-
await tracker1.save();
|
|
46
|
-
|
|
47
|
-
// Create second tracker and verify data loaded
|
|
48
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
49
|
-
await tracker2.load();
|
|
50
|
-
|
|
51
|
-
const cursorMetrics = await tracker2.getHealthMetrics('cursor');
|
|
52
|
-
const windsurfMetrics = await tracker2.getHealthMetrics('windsurf');
|
|
53
|
-
const vscodeMetrics = await tracker2.getHealthMetrics('vscode');
|
|
54
|
-
|
|
55
|
-
// Verify Cursor data
|
|
56
|
-
expect(cursorMetrics.successCount).toBe(2);
|
|
57
|
-
expect(cursorMetrics.failureCount).toBe(0);
|
|
58
|
-
expect(cursorMetrics.averageResponseTime).toBeGreaterThan(0);
|
|
59
|
-
|
|
60
|
-
// Verify Windsurf data
|
|
61
|
-
expect(windsurfMetrics.successCount).toBe(0);
|
|
62
|
-
expect(windsurfMetrics.failureCount).toBe(1);
|
|
63
|
-
|
|
64
|
-
// Verify VS Code quota didn't increment counters
|
|
65
|
-
expect(vscodeMetrics.successCount).toBe(0);
|
|
66
|
-
expect(vscodeMetrics.failureCount).toBe(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// KNOWN LIMITATION: Concurrent writes with autoSave:false not currently supported
|
|
70
|
-
// The current implementation writes to disk on every operation, which causes race
|
|
71
|
-
// conditions when multiple operations run concurrently. This would require in-memory
|
|
72
|
-
// caching to fix properly. In production, autoSave defaults to true with 500ms
|
|
73
|
-
// debouncing, which prevents this issue.
|
|
74
|
-
it.skip('should handle concurrent writes from same tracker', async () => {
|
|
75
|
-
const tracker = new IDEHealthTracker({
|
|
76
|
-
storageFile: testStorageFile,
|
|
77
|
-
autoSave: false,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Record multiple interactions rapidly
|
|
81
|
-
const promises = [];
|
|
82
|
-
for (let i = 0; i < 20; i++) {
|
|
83
|
-
promises.push(tracker.recordSuccess('cursor', 120000 + i * 100));
|
|
84
|
-
}
|
|
85
|
-
await Promise.all(promises);
|
|
86
|
-
|
|
87
|
-
await tracker.save();
|
|
88
|
-
|
|
89
|
-
// Verify all were recorded
|
|
90
|
-
const metrics = await tracker.getHealthMetrics('cursor');
|
|
91
|
-
expect(metrics.successCount).toBe(20);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should create backup before overwriting', async () => {
|
|
95
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
96
|
-
await tracker1.recordSuccess('cursor', 120000);
|
|
97
|
-
await tracker1.save();
|
|
98
|
-
|
|
99
|
-
// Verify file exists
|
|
100
|
-
const fileExists = await fs.pathExists(testStorageFile);
|
|
101
|
-
expect(fileExists).toBe(true);
|
|
102
|
-
|
|
103
|
-
// Record more data (should create backup)
|
|
104
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
105
|
-
await tracker2.recordSuccess('windsurf', 180000);
|
|
106
|
-
await tracker2.save();
|
|
107
|
-
|
|
108
|
-
// Verify backup was created
|
|
109
|
-
const backupFile = `${testStorageFile}.bak`;
|
|
110
|
-
const backupExists = await fs.pathExists(backupFile);
|
|
111
|
-
expect(backupExists).toBe(true);
|
|
112
|
-
|
|
113
|
-
// Verify backup contains original data
|
|
114
|
-
const backupData = await fs.readJson(backupFile);
|
|
115
|
-
expect(backupData.ides.cursor).toBeDefined();
|
|
116
|
-
expect(backupData.ides.cursor.successCount).toBe(1);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should restore from backup on corrupted data', async () => {
|
|
120
|
-
// Create valid data
|
|
121
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
122
|
-
await tracker1.recordSuccess('cursor', 120000);
|
|
123
|
-
await tracker1.save();
|
|
124
|
-
|
|
125
|
-
// Corrupt the main file
|
|
126
|
-
await fs.writeFile(testStorageFile, 'invalid json {{{');
|
|
127
|
-
|
|
128
|
-
// Try to load - should restore from backup
|
|
129
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
130
|
-
const metrics = await tracker2.getHealthMetrics('cursor');
|
|
131
|
-
|
|
132
|
-
// Should have loaded from backup
|
|
133
|
-
expect(metrics.successCount).toBe(1);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('Multi-IDE Scenarios', () => {
|
|
138
|
-
it('should track multiple IDEs independently', async () => {
|
|
139
|
-
const tracker = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
140
|
-
|
|
141
|
-
// Record interactions for different IDEs
|
|
142
|
-
await tracker.recordSuccess('cursor', 120000);
|
|
143
|
-
await tracker.recordSuccess('cursor', 115000);
|
|
144
|
-
await tracker.recordSuccess('windsurf', 200000);
|
|
145
|
-
await tracker.recordFailure('vscode', 'Error');
|
|
146
|
-
await tracker.recordQuota('github-copilot', 'Quota exceeded');
|
|
147
|
-
|
|
148
|
-
await tracker.save();
|
|
149
|
-
|
|
150
|
-
// Verify each IDE tracked separately
|
|
151
|
-
const allMetrics = await tracker.getAllHealthMetrics();
|
|
152
|
-
expect(allMetrics.size).toBe(4); // cursor, windsurf, vscode, github-copilot
|
|
153
|
-
|
|
154
|
-
expect(allMetrics.get('cursor').successCount).toBe(2);
|
|
155
|
-
expect(allMetrics.get('windsurf').successCount).toBe(1);
|
|
156
|
-
expect(allMetrics.get('vscode').failureCount).toBe(1);
|
|
157
|
-
expect(allMetrics.get('github-copilot').successCount).toBe(0);
|
|
158
|
-
expect(allMetrics.get('github-copilot').failureCount).toBe(0);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should recommend best IDE based on success rate', async () => {
|
|
162
|
-
const tracker = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
163
|
-
|
|
164
|
-
// Cursor: 80% success (8/10)
|
|
165
|
-
for (let i = 0; i < 8; i++) {
|
|
166
|
-
await tracker.recordSuccess('cursor', 120000);
|
|
167
|
-
}
|
|
168
|
-
for (let i = 0; i < 2; i++) {
|
|
169
|
-
await tracker.recordFailure('cursor', 'Error');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Windsurf: 60% success (6/10)
|
|
173
|
-
for (let i = 0; i < 6; i++) {
|
|
174
|
-
await tracker.recordSuccess('windsurf', 180000);
|
|
175
|
-
}
|
|
176
|
-
for (let i = 0; i < 4; i++) {
|
|
177
|
-
await tracker.recordFailure('windsurf', 'Error');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// VS Code: 100% but only 2 interactions (below threshold)
|
|
181
|
-
for (let i = 0; i < 2; i++) {
|
|
182
|
-
await tracker.recordSuccess('vscode', 150000);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const recommended = await tracker.getRecommendedIDE({ minInteractions: 10 });
|
|
186
|
-
expect(recommended).toBe('cursor');
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('EWMA Calculation Persistence', () => {
|
|
191
|
-
it('should persist and recalculate EWMA correctly', async () => {
|
|
192
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
193
|
-
|
|
194
|
-
// Record response times with increasing pattern
|
|
195
|
-
await tracker1.recordSuccess('cursor', 100000);
|
|
196
|
-
await tracker1.recordSuccess('cursor', 110000);
|
|
197
|
-
await tracker1.recordSuccess('cursor', 120000);
|
|
198
|
-
await tracker1.save();
|
|
199
|
-
|
|
200
|
-
const metrics1 = await tracker1.getHealthMetrics('cursor');
|
|
201
|
-
const ewma1 = metrics1.averageResponseTime;
|
|
202
|
-
expect(ewma1).toBeGreaterThan(100000);
|
|
203
|
-
expect(ewma1).toBeLessThan(120000);
|
|
204
|
-
|
|
205
|
-
// Load in new instance and add more data
|
|
206
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
207
|
-
await tracker2.load();
|
|
208
|
-
|
|
209
|
-
await tracker2.recordSuccess('cursor', 130000);
|
|
210
|
-
const metrics2 = await tracker2.getHealthMetrics('cursor');
|
|
211
|
-
const ewma2 = metrics2.averageResponseTime;
|
|
212
|
-
|
|
213
|
-
// EWMA should have increased
|
|
214
|
-
expect(ewma2).toBeGreaterThan(ewma1);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Consecutive Failures Across Sessions', () => {
|
|
219
|
-
it('should maintain consecutive failure count across restarts', async () => {
|
|
220
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
221
|
-
|
|
222
|
-
// Record 3 failures
|
|
223
|
-
await tracker1.recordFailure('cursor', 'Error 1');
|
|
224
|
-
await tracker1.recordFailure('cursor', 'Error 2');
|
|
225
|
-
await tracker1.recordFailure('cursor', 'Error 3');
|
|
226
|
-
await tracker1.save();
|
|
227
|
-
|
|
228
|
-
// Load in new instance
|
|
229
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
230
|
-
await tracker2.load();
|
|
231
|
-
|
|
232
|
-
const metrics = await tracker2.getHealthMetrics('cursor');
|
|
233
|
-
expect(metrics.consecutiveFailures).toBe(3);
|
|
234
|
-
|
|
235
|
-
// Add 2 more failures to trigger threshold
|
|
236
|
-
let eventEmitted = false;
|
|
237
|
-
tracker2.on('consecutive-failures', () => {
|
|
238
|
-
eventEmitted = true;
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await tracker2.recordFailure('cursor', 'Error 4');
|
|
242
|
-
await tracker2.recordFailure('cursor', 'Error 5');
|
|
243
|
-
|
|
244
|
-
expect(eventEmitted).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should reset consecutive failures on success across sessions', async () => {
|
|
248
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
249
|
-
|
|
250
|
-
// Record failures
|
|
251
|
-
await tracker1.recordFailure('cursor', 'Error 1');
|
|
252
|
-
await tracker1.recordFailure('cursor', 'Error 2');
|
|
253
|
-
await tracker1.save();
|
|
254
|
-
|
|
255
|
-
// Load in new instance and record success
|
|
256
|
-
const tracker2 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
257
|
-
await tracker2.load();
|
|
258
|
-
|
|
259
|
-
await tracker2.recordSuccess('cursor', 120000);
|
|
260
|
-
|
|
261
|
-
const metrics = await tracker2.getHealthMetrics('cursor');
|
|
262
|
-
expect(metrics.consecutiveFailures).toBe(0);
|
|
263
|
-
expect(metrics.successCount).toBe(1);
|
|
264
|
-
expect(metrics.failureCount).toBe(2);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
describe('Timeout Configuration Persistence', () => {
|
|
269
|
-
it('should persist timeout configuration', async () => {
|
|
270
|
-
const tracker1 = new IDEHealthTracker({ storageFile: testStorageFile });
|
|
271
|
-
|
|
272
|
-
// Record some data
|
|
273
|
-
await tracker1.recordSuccess('cursor', 120000);
|
|
274
|
-
await tracker1.save();
|
|
275
|
-
|
|
276
|
-
// Verify timeout config persisted
|
|
277
|
-
const data = await fs.readJson(testStorageFile);
|
|
278
|
-
expect(data.timeoutConfig).toBeDefined();
|
|
279
|
-
expect(data.timeoutConfig.mode).toBe('fixed');
|
|
280
|
-
expect(data.timeoutConfig.defaultTimeout).toBe(1800000);
|
|
281
|
-
expect(data.timeoutConfig.bufferPercentage).toBe(0.4);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|