Cleanup worker-service.ts: remove dead code and fallback concept (#706)

* refactor(worker): remove dead code from worker-service.ts

Remove ~216 lines of unreachable code:
- Delete `runInteractiveSetup` function (defined but never called)
- Remove unused imports: fs namespace, spawn, homedir, readline,
  existsSync/writeFileSync/readFileSync/mkdirSync
- Clean up CursorHooksInstaller imports (keep only used exports)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(worker): only enable SDK fallback when Claude is configured

Add isConfigured() method to SDKAgent that checks for ANTHROPIC_API_KEY
or claude CLI availability. Worker now only sets SDK agent as fallback
for third-party providers when credentials exist, preventing cascading
failures for users who intentionally use Gemini/OpenRouter without Claude.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(worker): remove misleading re-export indirection

Remove unnecessary re-export of updateCursorContextForProject from
worker-service.ts. ResponseProcessor now imports directly from
CursorHooksInstaller.ts where the function is defined. This eliminates
misleading indirection that suggested a circular dependency existed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(mcp): use build-time injected version instead of hardcoded strings

Replace hardcoded '1.0.0' version strings with __DEFAULT_PACKAGE_VERSION__
constant that esbuild replaces at build time. This ensures MCP server and
client versions stay synchronized with package.json.

- worker-service.ts: MCP client version now uses packageVersion
- ChromaSync.ts: MCP client version now uses packageVersion
- mcp-server.ts: MCP server version now uses packageVersion
- Added clarifying comments for empty MCP capabilities objects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Implement cleanup and validation plans for worker-service.ts

- Added a comprehensive cleanup plan addressing 23 identified issues in worker-service.ts, focusing on safe deletions, low-risk simplifications, and medium-risk improvements.
- Created an execution plan for validating intentional patterns in worker-service.ts, detailing necessary actions and priorities.
- Generated a report on unjustified logic in worker-service.ts, categorizing issues by severity and providing recommendations for immediate and short-term actions.
- Introduced documentation for recent activity in the mem-search plugin, enhancing traceability and context for changes.

* fix(sdk): remove dangerous ANTHROPIC_API_KEY check from isConfigured

Claude Code uses CLI authentication, not direct API calls. Checking for
ANTHROPIC_API_KEY could accidentally use a user's API key (from other
projects) which costs 20x more than Claude Code's pricing.

Now only checks for claude CLI availability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(worker): remove fallback agent concept entirely

Users who choose Gemini/OpenRouter want those providers, not secret
fallback behavior. Removed setFallbackAgent calls and the unused
isConfigured() method.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-01-13 23:30:13 -05:00
committed by GitHub
parent c314946204
commit 05323c9db5
18 changed files with 1836 additions and 882 deletions
-344
View File
@@ -1,344 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { execSync, spawn } from 'child_process';
import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, chmodSync } from 'fs';
import { join } from 'path';
import { tmpdir, homedir } from 'os';
/**
* Tests for Cursor Hook Script Outputs
*
* These tests validate that hook scripts produce the correct JSON output
* required by Cursor's hook system.
*
* Critical requirements:
* - beforeSubmitPrompt hooks MUST output {"continue": true}
* - stop hooks MUST output valid JSON (usually {} or {"followup_message": "..."})
*
* If these outputs are wrong, Cursor will block prompts or fail silently.
*/
// Skip these tests if jq is not installed (required by the scripts)
function hasJq(): boolean {
try {
execSync('which jq', { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
// Skip these tests on Windows (bash scripts)
function isUnix(): boolean {
return process.platform !== 'win32';
}
const describeOrSkip = (hasJq() && isUnix()) ? describe : describe.skip;
describeOrSkip('Cursor Hook Script Outputs', () => {
let tempDir: string;
let cursorHooksDir: string;
beforeEach(() => {
// Create unique temp directory for each test
tempDir = join(tmpdir(), `cursor-hook-output-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(tempDir, { recursive: true });
// Find cursor-hooks directory
cursorHooksDir = join(process.cwd(), 'cursor-hooks');
if (!existsSync(cursorHooksDir)) {
throw new Error('cursor-hooks directory not found');
}
});
afterEach(() => {
// Clean up temp directory
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
/**
* Run a hook script with input and return the output
*/
function runHookScript(scriptName: string, input: object): string {
const scriptPath = join(cursorHooksDir, scriptName);
if (!existsSync(scriptPath)) {
throw new Error(`Script not found: ${scriptPath}`);
}
// Make sure script is executable
chmodSync(scriptPath, 0o755);
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
env: {
...process.env,
HOME: homedir(), // Ensure HOME is set for ~/.claude-mem access
},
encoding: 'utf-8',
timeout: 10000,
});
return result.trim();
}
describe('session-init.sh (beforeSubmitPrompt)', () => {
it('outputs {"continue": true} for valid input', () => {
const input = {
conversation_id: 'test-conv-123',
prompt: 'Hello world',
workspace_roots: [tempDir]
};
const output = runHookScript('session-init.sh', input);
const parsed = JSON.parse(output);
expect(parsed.continue).toBe(true);
});
it('outputs {"continue": true} even with empty input', () => {
const output = runHookScript('session-init.sh', {});
const parsed = JSON.parse(output);
expect(parsed.continue).toBe(true);
});
it('outputs {"continue": true} even with invalid JSON-like input', () => {
const input = {
conversation_id: null,
workspace_roots: null
};
const output = runHookScript('session-init.sh', input);
const parsed = JSON.parse(output);
expect(parsed.continue).toBe(true);
});
it('output is valid JSON', () => {
const input = {
conversation_id: 'test-123',
prompt: 'Test prompt'
};
const output = runHookScript('session-init.sh', input);
// Should not throw
expect(() => JSON.parse(output)).not.toThrow();
});
});
describe('context-inject.sh (beforeSubmitPrompt)', () => {
it('outputs {"continue": true} for valid input', () => {
const input = {
workspace_roots: [tempDir]
};
const output = runHookScript('context-inject.sh', input);
const parsed = JSON.parse(output);
expect(parsed.continue).toBe(true);
});
it('outputs {"continue": true} even with empty input', () => {
const output = runHookScript('context-inject.sh', {});
const parsed = JSON.parse(output);
expect(parsed.continue).toBe(true);
});
it('output is valid JSON', () => {
const output = runHookScript('context-inject.sh', {});
expect(() => JSON.parse(output)).not.toThrow();
});
});
describe('session-summary.sh (stop)', () => {
it('outputs valid JSON for typical input', () => {
const input = {
conversation_id: 'test-conv-456',
workspace_roots: [tempDir],
status: 'completed'
};
const output = runHookScript('session-summary.sh', input);
// Should be valid JSON
expect(() => JSON.parse(output)).not.toThrow();
});
it('outputs empty object {} when nothing to report', () => {
const input = {
// No conversation_id - should exit early with {}
};
const output = runHookScript('session-summary.sh', input);
const parsed = JSON.parse(output);
expect(parsed).toEqual({});
});
it('output is valid JSON even with minimal input', () => {
const output = runHookScript('session-summary.sh', {});
expect(() => JSON.parse(output)).not.toThrow();
});
});
describe('save-observation.sh (afterMCPExecution)', () => {
it('exits cleanly with no output for valid MCP input', () => {
const input = {
conversation_id: 'test-conv-789',
hook_event_name: 'afterMCPExecution',
tool_name: 'Bash',
tool_input: { command: 'ls' },
result_json: { output: 'file1.txt' },
workspace_roots: [tempDir]
};
// This script should exit with 0 and produce no output
const scriptPath = join(cursorHooksDir, 'save-observation.sh');
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should be empty or just whitespace
expect(result.trim()).toBe('');
});
it('exits cleanly for shell execution input', () => {
const input = {
conversation_id: 'test-conv-101',
hook_event_name: 'afterShellExecution',
command: 'ls -la',
output: 'file1.txt\nfile2.txt',
workspace_roots: [tempDir]
};
const scriptPath = join(cursorHooksDir, 'save-observation.sh');
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should be empty or just whitespace
expect(result.trim()).toBe('');
});
it('exits cleanly with no session_id', () => {
const input = {
hook_event_name: 'afterMCPExecution',
tool_name: 'Bash'
// No conversation_id or generation_id
};
const scriptPath = join(cursorHooksDir, 'save-observation.sh');
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should exit cleanly
expect(result.trim()).toBe('');
});
});
describe('save-file-edit.sh (afterFileEdit)', () => {
it('exits cleanly with valid file edit input', () => {
const input = {
conversation_id: 'test-conv-edit',
file_path: '/path/to/file.ts',
edits: [
{ old_string: 'old code', new_string: 'new code' }
],
workspace_roots: [tempDir]
};
const scriptPath = join(cursorHooksDir, 'save-file-edit.sh');
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should be empty or just whitespace
expect(result.trim()).toBe('');
});
it('exits cleanly with no file_path', () => {
const input = {
conversation_id: 'test-conv-edit',
edits: []
// No file_path - should exit early
};
const scriptPath = join(cursorHooksDir, 'save-file-edit.sh');
const result = execSync(`bash "${scriptPath}"`, {
input: JSON.stringify(input),
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should exit cleanly
expect(result.trim()).toBe('');
});
});
describe('script error handling', () => {
it('session-init.sh never outputs error to stdout', () => {
// Even with completely broken input, should still output valid JSON
const scriptPath = join(cursorHooksDir, 'session-init.sh');
// Pass invalid input that might cause jq errors
const result = execSync(`echo '{}' | bash "${scriptPath}"`, {
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Output should still be valid JSON with continue: true
const parsed = JSON.parse(result.trim());
expect(parsed.continue).toBe(true);
});
it('context-inject.sh never outputs error to stdout', () => {
const scriptPath = join(cursorHooksDir, 'context-inject.sh');
const result = execSync(`echo '{}' | bash "${scriptPath}"`, {
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
const parsed = JSON.parse(result.trim());
expect(parsed.continue).toBe(true);
});
it('session-summary.sh never outputs error to stdout', () => {
const scriptPath = join(cursorHooksDir, 'session-summary.sh');
const result = execSync(`echo '{}' | bash "${scriptPath}"`, {
cwd: tempDir,
encoding: 'utf-8',
timeout: 10000,
});
// Should be valid JSON
expect(() => JSON.parse(result.trim())).not.toThrow();
});
});
});
@@ -81,23 +81,19 @@ describe('Version Consistency', () => {
expect(matches!.length).toBeGreaterThan(0);
});
it('should have version injected into built mcp-server.cjs', () => {
it('should have built mcp-server.cjs', () => {
const mcpServerPath = path.join(projectRoot, 'plugin/scripts/mcp-server.cjs');
// Skip if file doesn't exist (e.g., before first build)
if (!existsSync(mcpServerPath)) {
console.log('⚠️ mcp-server.cjs not found - run npm run build first');
return;
}
// mcp-server.cjs doesn't use __DEFAULT_PACKAGE_VERSION__ - it's a search server
// that doesn't need to expose version info. Just verify it exists and is built.
const mcpServerContent = readFileSync(mcpServerPath, 'utf-8');
// Check for the version string in the minified code
const versionPattern = new RegExp(`"${rootVersion.replace(/\./g, '\\.')}"`, 'g');
const matches = mcpServerContent.match(versionPattern);
expect(matches).toBeTruthy();
expect(matches!.length).toBeGreaterThan(0);
expect(mcpServerContent.length).toBeGreaterThan(0);
});
it('should validate version format is semver compliant', () => {
+2
View File
@@ -35,6 +35,8 @@ const EXCLUDED_PATTERNS = [
/integrations\/.*Installer\.ts$/, // CLI installer commands (console.log for interactive installation output)
/SettingsDefaultsManager\.ts$/, // Must use console.log to avoid circular dependency with logger
/user-message-hook\.ts$/, // Deprecated - kept for reference only, not registered in hooks.json
/cli\/hook-command\.ts$/, // CLI hook command uses console.log/error for hook protocol output
/cli\/handlers\/user-message\.ts$/, // User message handler uses console.error for user-visible context
];
// Files that should always use logger (core business logic)