Skip to content

Commit d559afe

Browse files
jrenaldi79claude
andcommitted
fix: 6 verified bugs from PR #9 review
Each bug was independently reproduced before fixing: - api-key-validation: Anthropic 429/5xx incorrectly resolved as valid key - api-key-validation: no HTTP timeout, requests hang forever on unreachable providers - setup-window: remote debug port 9222 always enabled (now opt-in via SIDECAR_DEBUG_PORT) - ipc-setup: get-api-keys handler has no error handling, saveApiKey failures ignored - cli: --key=value syntax parsed as {"model=gemini": true} instead of {"model": "gemini"} - pre-push hook: test failures don't block push (exits 0 due to npm audit fallback) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ad433c5 commit d559afe

File tree

7 files changed

+80
-32
lines changed

7 files changed

+80
-32
lines changed

.husky/pre-push

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ if [ -n "$HEAD_SHA" ] && [ "$HEAD_SHA" = "$CACHED_SHA" ]; then
1515
echo "Tests already passed for $HEAD_SHA — skipping."
1616
else
1717
echo "Running full test suite (unit + integration) before push..."
18-
npm run test:all
18+
npm run test:all || exit 1
1919
fi
2020

2121
echo "Checking for dependency vulnerabilities..."

electron/ipc-setup.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,28 @@ function registerSetupHandlers(ipcMain, getMainWindow) {
9999
});
100100

101101
ipcMain.handle('sidecar:get-api-keys', () => {
102-
const { readApiKeys, readApiKeyHints, saveApiKey } = require('../src/utils/api-key-store');
103-
const { importFromAuthJson } = require('../src/utils/auth-json');
104-
const status = readApiKeys();
105-
const hints = readApiKeyHints();
102+
try {
103+
const { readApiKeys, readApiKeyHints, saveApiKey } = require('../src/utils/api-key-store');
104+
const { importFromAuthJson } = require('../src/utils/auth-json');
105+
const status = readApiKeys();
106+
const hints = readApiKeyHints();
106107

107-
// Auto-import keys from auth.json that sidecar doesn't have yet
108-
const { imported } = importFromAuthJson(status);
109-
for (const entry of imported) {
110-
saveApiKey(entry.provider, entry.key);
111-
status[entry.provider] = true;
112-
const visible = entry.key.slice(0, 8);
113-
hints[entry.provider] = visible + '\u2022'.repeat(Math.max(0, Math.min(entry.key.length - 8, 12)));
114-
}
108+
// Auto-import keys from auth.json that sidecar doesn't have yet
109+
const { imported } = importFromAuthJson(status);
110+
for (const entry of imported) {
111+
const result = saveApiKey(entry.provider, entry.key);
112+
if (result && result.success !== false) {
113+
status[entry.provider] = true;
114+
const visible = entry.key.slice(0, 8);
115+
hints[entry.provider] = visible + '\u2022'.repeat(Math.max(0, Math.min(entry.key.length - 8, 12)));
116+
}
117+
}
115118

116-
return { status, hints, imported: imported.map(e => e.provider) };
119+
return { status, hints, imported: imported.map(e => e.provider) };
120+
} catch (err) {
121+
logger.error('get-api-keys handler error', { error: err.message });
122+
return { status: {}, hints: {}, imported: [] };
123+
}
117124
});
118125

119126
ipcMain.handle('sidecar:fetch-models', async () => {

src/cli.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,29 @@ function parseArgs(argv) {
4747
const arg = argv[i];
4848

4949
if (arg.startsWith('--')) {
50-
const key = arg.slice(2);
50+
let key = arg.slice(2);
5151
const next = argv[i + 1];
5252

53+
// Handle --key=value syntax
54+
let inlineValue;
55+
const eqIdx = key.indexOf('=');
56+
if (eqIdx !== -1) {
57+
inlineValue = key.slice(eqIdx + 1);
58+
key = key.slice(0, eqIdx);
59+
}
60+
5361
// Boolean flags (no value expected)
5462
if (isBooleanFlag(key)) {
5563
result[key] = true;
5664
continue;
5765
}
5866

67+
// If --key=value was used, use the inline value directly
68+
if (inlineValue !== undefined) {
69+
result[key] = parseValue(key, inlineValue);
70+
continue;
71+
}
72+
5973
// Array accumulation flags
6074
if (key === 'exclude-mcp' && next && !next.startsWith('--')) {
6175
result['exclude-mcp'] = result['exclude-mcp'] || [];

src/sidecar/setup-window.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ function launchSetupWindow() {
2929
SIDECAR_MODE: 'setup'
3030
};
3131

32-
const debugPort = process.env.SIDECAR_DEBUG_PORT || '9222';
33-
logger.info('Launching setup window', { debugPort });
32+
const debugPort = process.env.SIDECAR_DEBUG_PORT;
33+
const args = debugPort
34+
? [`--remote-debugging-port=${debugPort}`, mainPath]
35+
: [mainPath];
36+
logger.info('Launching setup window', { debugPort: debugPort || 'disabled' });
3437

35-
const proc = spawn(electronPath, [
36-
`--remote-debugging-port=${debugPort}`,
37-
mainPath
38-
], {
38+
const proc = spawn(electronPath, args, {
3939
env,
4040
stdio: ['ignore', 'pipe', 'pipe']
4141
});

src/utils/api-key-validation.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ function validateApiKey(provider, key) {
6060
if (provider === 'anthropic') {
6161
if (res.statusCode === 401) {
6262
resolve({ valid: false, error: 'Invalid API key (401)' });
63+
} else if (res.statusCode >= 500 || res.statusCode === 429) {
64+
resolve({ valid: false, error: `Server error (${res.statusCode})` });
6365
} else {
6466
resolve({ valid: true });
6567
}
@@ -75,6 +77,10 @@ function validateApiKey(provider, key) {
7577
}
7678
});
7779
});
80+
req.setTimeout(10000, () => {
81+
req.destroy();
82+
resolve({ valid: false, error: 'Request timed out' });
83+
});
7884
req.on('error', (err) => {
7985
resolve({ valid: false, error: err.message });
8086
});

tests/api-key-store-validation.test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('api-key-store validation', () => {
5252
};
5353
https.get.mockImplementation((_url, _opts, cb) => {
5454
cb(mockResponse);
55-
return { on: jest.fn() };
55+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
5656
});
5757

5858
const result = await validateApiKey('openrouter', 'sk-or-valid');
@@ -70,7 +70,7 @@ describe('api-key-store validation', () => {
7070
};
7171
https.get.mockImplementation((_url, _opts, cb) => {
7272
cb(mockResponse);
73-
return { on: jest.fn() };
73+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
7474
});
7575

7676
const result = await validateApiKey('openrouter', 'sk-or-bad');
@@ -79,7 +79,7 @@ describe('api-key-store validation', () => {
7979

8080
it('should resolve invalid for network error', async () => {
8181
https.get.mockImplementation((_url, _opts, _cb) => {
82-
const req = { on: jest.fn() };
82+
const req = { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
8383
setTimeout(() => {
8484
const errCall = req.on.mock.calls.find(c => c[0] === 'error');
8585
if (errCall) { errCall[1](new Error('Network error')); }
@@ -122,7 +122,7 @@ describe('api-key-store validation', () => {
122122
};
123123
https.get.mockImplementation((_url, _opts, cb) => {
124124
cb(mockResponse);
125-
return { on: jest.fn() };
125+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
126126
});
127127

128128
const result = await validateApiKey('openrouter', 'sk-or-forbidden');
@@ -140,7 +140,7 @@ describe('api-key-store validation', () => {
140140
};
141141
https.get.mockImplementation((_url, _opts, cb) => {
142142
cb(mockResponse);
143-
return { on: jest.fn() };
143+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
144144
});
145145

146146
const result = await validateApiKey('openrouter', 'sk-or-500');
@@ -159,7 +159,7 @@ describe('api-key-store validation', () => {
159159
};
160160
https.get.mockImplementation((_url, _opts, cb) => {
161161
cb(mockResponse);
162-
return { on: jest.fn() };
162+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
163163
});
164164

165165
const result = await validateApiKey('anthropic', 'sk-ant-valid');
@@ -177,7 +177,7 @@ describe('api-key-store validation', () => {
177177
};
178178
https.get.mockImplementation((_url, _opts, cb) => {
179179
cb(mockResponse);
180-
return { on: jest.fn() };
180+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
181181
});
182182

183183
const result = await validateApiKey('anthropic', 'sk-ant-bad');
@@ -199,7 +199,7 @@ describe('api-key-store validation', () => {
199199
capturedUrl = url;
200200
capturedHeaders = opts.headers;
201201
cb(mockResponse);
202-
return { on: jest.fn() };
202+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
203203
});
204204

205205
const result = await validateApiKey('deepseek', 'sk-deepseek-valid');
@@ -221,7 +221,7 @@ describe('api-key-store validation', () => {
221221
https.get.mockImplementation((url, _opts, cb) => {
222222
capturedUrl = url;
223223
cb(mockResponse);
224-
return { on: jest.fn() };
224+
return { on: jest.fn(), setTimeout: jest.fn(), destroy: jest.fn() };
225225
});
226226

227227
await validateApiKey('google', 'AIza-test-key');

tests/sidecar/setup-window.test.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,36 @@ describe('setup-window', () => {
5454

5555
expect(spawn).toHaveBeenCalled();
5656
const spawnArgs = spawn.mock.calls[0];
57-
expect(spawnArgs[1][0]).toContain('--remote-debugging-port=');
58-
expect(spawnArgs[1][1]).toContain('main.js');
57+
// Debug port is opt-in: without SIDECAR_DEBUG_PORT, only main.js is passed
58+
expect(spawnArgs[1][0]).toContain('main.js');
5959

6060
const env = spawnArgs[2].env;
6161
expect(env.SIDECAR_MODE).toBe('setup');
6262

6363
expect(result.success).toBe(true);
6464
});
6565

66+
it('should pass --remote-debugging-port when SIDECAR_DEBUG_PORT is set', async () => {
67+
process.env.SIDECAR_DEBUG_PORT = '9333';
68+
spawn.mockClear();
69+
spawn.mockReturnValue(mockProcess);
70+
71+
const promise = launchSetupWindow();
72+
73+
const dataCallback = mockProcess.stdout.on.mock.calls.find(c => c[0] === 'data')[1];
74+
dataCallback('{"status":"complete"}\n');
75+
76+
const closeCallback = mockProcess.on.mock.calls.find(c => c[0] === 'close')[1];
77+
closeCallback(0);
78+
79+
await promise;
80+
81+
const spawnArgs = spawn.mock.calls[0];
82+
expect(spawnArgs[1][0]).toBe('--remote-debugging-port=9333');
83+
expect(spawnArgs[1][1]).toContain('main.js');
84+
delete process.env.SIDECAR_DEBUG_PORT;
85+
});
86+
6687
it('should parse enriched JSON with default model and keyCount', async () => {
6788
const promise = launchSetupWindow();
6889

0 commit comments

Comments
 (0)