From 67290e85c0c513f31f8f648a33817d1f71dcd835 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 16 Mar 2026 14:55:53 -0400 Subject: [PATCH] test(cursor): reclassify daemon lifecycle smoke coverage --- tests/README.md | 24 +-- .../cursor-daemon-lifecycle.test.ts | 165 ++++++++++++++++++ tests/unit/cursor/cursor-daemon.test.ts | 154 +--------------- 3 files changed, 179 insertions(+), 164 deletions(-) create mode 100644 tests/integration/cursor-daemon-lifecycle.test.ts diff --git a/tests/README.md b/tests/README.md index 3f0091ed..3abf42e6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,7 +11,7 @@ tests/ ├── native/ # Native installation tests (bash/PowerShell) │ ├── unix/ # Unix/Linux/macOS tests │ └── windows/ # Windows PowerShell tests -├── integration/ # Integration tests (manual execution) +├── integration/ # Integration + smoke tests └── shared/ # Shared utilities ├── fixtures/ # Test configuration and environment ├── unit/ # Helper function tests @@ -22,9 +22,9 @@ tests/ ## Running Tests ```bash -bun run test # All automated tests (unit + npm) -bun run test:unit # Unit tests only (Mocha) -bun run test:npm # npm package tests (Mocha) +bun run test # All automated tests (unit + integration + npm) +bun run test:unit # Unit tests only +bun run test:npm # npm package tests bun run test:native # Native Unix tests (bash) ``` @@ -48,16 +48,18 @@ Installation tests for curl|bash (Unix) and irm|iex (Windows): - `native/windows/edge-cases.ps1` - Windows edge case tests ### Integration Tests (`integration/`) -Manual execution tests for specific scenarios: -- `token-counting-test.js` - Token counting validation -- `z-ai-streaming-test.js` - Z.AI streaming -- `glmt-integration-test.sh` - GLMT integration +Integration and smoke coverage for scenarios that exercise multiple layers: +- Automated `*.test.ts` files run as part of `bun run test:all` and CI +- Shell and standalone probe scripts remain on-demand for targeted debugging +- `cursor-daemon-lifecycle.test.ts` - local daemon process + HTTP smoke coverage +- `image-analyzer-hook.test.ts` - hook integration coverage +- `glmt-integration-test.sh` - GLMT integration probe - `symlink-chain-test.sh` - Symlink chain handling - `ux-integration-test.sh` - CLI UX integration ## Adding New Tests -- **Unit tests**: Add to `unit//` using Mocha + Node.js assert -- **npm tests**: Add to `npm/` using Mocha +- **Unit tests**: Add to `unit//` for isolated module behavior +- **npm tests**: Add to `npm/` for package behavior - **Native tests**: Add to `native/unix/` or `native/windows/` -- **Integration tests**: Add to `integration/` +- **Integration tests**: Add automated cross-layer smoke coverage to `integration/*.test.ts` diff --git a/tests/integration/cursor-daemon-lifecycle.test.ts b/tests/integration/cursor-daemon-lifecycle.test.ts new file mode 100644 index 00000000..db76fcbd --- /dev/null +++ b/tests/integration/cursor-daemon-lifecycle.test.ts @@ -0,0 +1,165 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { isDaemonRunning, startDaemon, stopDaemon } from '../../src/cursor/cursor-daemon'; +import { saveCredentials } from '../../src/cursor/cursor-auth'; + +let originalCcsHome: string | undefined; +let tempDir: string; + +beforeEach(() => { + originalCcsHome = process.env.CCS_HOME; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-daemon-integration-')); + process.env.CCS_HOME = tempDir; +}); + +afterEach(async () => { + await stopDaemon(); + + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } +}); + +describe('cursor daemon lifecycle smoke', () => { + it('starts, serves expected routes, and stops cleanly', async () => { + const port = 10000 + Math.floor(Math.random() * 50000); + const result = await startDaemon({ port, ghost_mode: true }); + expect(result.success).toBe(true); + expect(result.pid).toBeDefined(); + + expect(await isDaemonRunning(port)).toBe(true); + + const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`); + expect(modelsResponse.status).toBe(200); + const modelsJson = (await modelsResponse.json()) as { object?: string; data?: unknown[] }; + expect(modelsJson.object).toBe('list'); + expect(Array.isArray(modelsJson.data)).toBe(true); + + const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4.1', + messages: [{ role: 'user', content: 'hello' }], + }), + }); + expect(chatResponse.status).toBe(401); + + const anthropicResponse = await fetch(`http://127.0.0.1:${port}/v1/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4.5', + max_tokens: 256, + messages: [{ role: 'user', content: 'hello' }], + }), + }); + expect(anthropicResponse.status).toBe(401); + + const stopResult = await stopDaemon(); + expect(stopResult.success).toBe(true); + expect(await isDaemonRunning(port)).toBe(false); + }, 35000); + + it('returns 404 for unknown routes', async () => { + const port = 10000 + Math.floor(Math.random() * 50000); + const result = await startDaemon({ port, ghost_mode: true }); + expect(result.success).toBe(true); + + const response = await fetch(`http://127.0.0.1:${port}/unknown`); + expect(response.status).toBe(404); + }); + + it('returns 401 when credentials are expired', async () => { + const port = 10000 + Math.floor(Math.random() * 50000); + const expiredAt = new Date(Date.now() - 26 * 60 * 60 * 1000).toISOString(); + + saveCredentials({ + accessToken: 'a'.repeat(60), + machineId: '1234567890abcdef1234567890abcdef', + authMethod: 'manual', + importedAt: expiredAt, + }); + + const result = await startDaemon({ port, ghost_mode: true }); + expect(result.success).toBe(true); + + const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4.1', + messages: [{ role: 'user', content: 'hello' }], + }), + }); + + expect(response.status).toBe(401); + const body = (await response.json()) as { error?: { message?: string } }; + expect(body.error?.message).toContain('expired'); + }); + + it('validates invalid JSON, invalid message schema, and oversized body', async () => { + const port = 10000 + Math.floor(Math.random() * 50000); + const result = await startDaemon({ port, ghost_mode: true }); + expect(result.success).toBe(true); + + const invalidJson = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{invalid-json', + }); + expect(invalidJson.status).toBe(400); + + const invalidSchema = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4.1', + messages: { role: 'user', content: 'hello' }, + }), + }); + expect(invalidSchema.status).toBe(400); + + const invalidAnthropic = await fetch(`http://127.0.0.1:${port}/v1/messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4.5', + max_tokens: 256, + messages: [{ role: 'user', content: [{ type: 'image' }] }], + }), + }); + expect(invalidAnthropic.status).toBe(400); + + const oversized = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4.1', + messages: [ + { + role: 'user', + content: 'x'.repeat(10 * 1024 * 1024 + 1024), + }, + ], + }), + }); + expect(oversized.status).toBe(413); + }); +}); diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index 8bc22d7a..3b5fc576 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -19,7 +19,7 @@ import { } from '../../../src/cursor/cursor-daemon'; import { getCcsDir } from '../../../src/utils/config-manager'; import { handleCursorCommand } from '../../../src/commands/cursor-command'; -import { loadCredentials, saveCredentials } from '../../../src/cursor/cursor-auth'; +import { loadCredentials } from '../../../src/cursor/cursor-auth'; // Test isolation let originalCcsHome: string | undefined; @@ -140,158 +140,6 @@ describe('startDaemon', () => { expect(result.success).toBe(false); expect(result.error).toContain('Invalid port'); }); - - it('starts and stops daemon successfully', async () => { - const port = 10000 + Math.floor(Math.random() * 50000); - const result = await startDaemon({ port, ghost_mode: true }); - expect(result.success).toBe(true); - expect(result.pid).toBeDefined(); - - // Verify health - const running = await isDaemonRunning(port); - expect(running).toBe(true); - - // Verify models endpoint exists and is OpenAI-compatible list shape - const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`); - expect(modelsResponse.status).toBe(200); - const modelsJson = (await modelsResponse.json()) as { object?: string; data?: unknown[] }; - expect(modelsJson.object).toBe('list'); - expect(Array.isArray(modelsJson.data)).toBe(true); - - // Verify chat endpoint exists (requires auth, should not be 404) - const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-4.1', - messages: [{ role: 'user', content: 'hello' }], - }), - }); - expect(chatResponse.status).toBe(401); - - const anthropicResponse = await fetch(`http://127.0.0.1:${port}/v1/messages`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify({ - model: 'claude-sonnet-4.5', - max_tokens: 256, - messages: [{ role: 'user', content: 'hello' }], - }), - }); - expect(anthropicResponse.status).toBe(401); - - // Stop - const stopResult = await stopDaemon(); - expect(stopResult.success).toBe(true); - - // Verify stopped - const stillRunning = await isDaemonRunning(port); - expect(stillRunning).toBe(false); - }, 35000); - - it('returns 404 for unknown routes', async () => { - const port = 10000 + Math.floor(Math.random() * 50000); - const result = await startDaemon({ port, ghost_mode: true }); - expect(result.success).toBe(true); - - try { - const response = await fetch(`http://127.0.0.1:${port}/unknown`); - expect(response.status).toBe(404); - } finally { - await stopDaemon(); - } - }); - - it('returns 401 when credentials are expired', async () => { - const port = 10000 + Math.floor(Math.random() * 50000); - const expiredAt = new Date(Date.now() - 26 * 60 * 60 * 1000).toISOString(); - - saveCredentials({ - accessToken: 'a'.repeat(60), - machineId: '1234567890abcdef1234567890abcdef', - authMethod: 'manual', - importedAt: expiredAt, - }); - - const result = await startDaemon({ port, ghost_mode: true }); - expect(result.success).toBe(true); - - try { - const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-4.1', - messages: [{ role: 'user', content: 'hello' }], - }), - }); - - expect(response.status).toBe(401); - const body = (await response.json()) as { error?: { message?: string } }; - expect(body.error?.message).toContain('expired'); - } finally { - await stopDaemon(); - } - }); - - it('validates invalid JSON, invalid message schema, and oversized body', async () => { - const port = 10000 + Math.floor(Math.random() * 50000); - const result = await startDaemon({ port, ghost_mode: true }); - expect(result.success).toBe(true); - - try { - const invalidJson = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: '{invalid-json', - }); - expect(invalidJson.status).toBe(400); - - const invalidSchema = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-4.1', - messages: { role: 'user', content: 'hello' }, - }), - }); - expect(invalidSchema.status).toBe(400); - - const invalidAnthropic = await fetch(`http://127.0.0.1:${port}/v1/messages`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'anthropic-version': '2023-06-01', - }, - body: JSON.stringify({ - model: 'claude-sonnet-4.5', - max_tokens: 256, - messages: [{ role: 'user', content: [{ type: 'image' }] }], - }), - }); - expect(invalidAnthropic.status).toBe(400); - - const oversized = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-4.1', - messages: [ - { - role: 'user', - content: 'x'.repeat(10 * 1024 * 1024 + 1024), - }, - ], - }), - }); - expect(oversized.status).toBe(413); - } finally { - await stopDaemon(); - } - }); }); describe('isDaemonRunning', () => {