mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
fix(cursor): propagate daemon auth token (#1616)
* fix(cursor): propagate daemon auth token * fix(cursor): align daemon auth token propagation
This commit is contained in:
1 parent
3f1dce30bc
commit
4283c4665c
7 files changed
+158
-46
No files matched your search
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '../cursor';
|
} from '../cursor';
|
||||||
|
|
||||||
import { DEFAULT_CURSOR_CONFIG } from '../config/unified-config-types';
|
import { DEFAULT_CURSOR_CONFIG } from '../config/unified-config-types';
|
||||||
|
import { getCursorDaemonToken } from '../cursor/cursor-daemon-auth';
|
||||||
import {
|
import {
|
||||||
renderCursorHelp,
|
renderCursorHelp,
|
||||||
renderCursorModels,
|
renderCursorModels,
|
||||||
@@ -253,6 +254,7 @@ async function handleStart(): Promise<number> {
|
|||||||
const result = await startDaemon({
|
const result = await startDaemon({
|
||||||
port: cursorConfig.port,
|
port: cursorConfig.port,
|
||||||
ghost_mode: cursorConfig.ghost_mode,
|
ghost_mode: cursorConfig.ghost_mode,
|
||||||
|
daemon_token: getCursorDaemonToken(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { translateAnthropicRequest } from './cursor-anthropic-translator';
|
|||||||
import { checkAuthStatus } from './cursor-auth';
|
import { checkAuthStatus } from './cursor-auth';
|
||||||
import { getModelsForDaemon, resolveCursorRequestModel } from './cursor-models';
|
import { getModelsForDaemon, resolveCursorRequestModel } from './cursor-models';
|
||||||
import type { CursorTool } from './cursor-protobuf-schema';
|
import type { CursorTool } from './cursor-protobuf-schema';
|
||||||
|
import { ValidationError } from '../errors/error-types';
|
||||||
|
|
||||||
interface DaemonRuntimeOptions {
|
interface DaemonRuntimeOptions {
|
||||||
port: number;
|
port: number;
|
||||||
@@ -132,22 +133,46 @@ function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function headerMatchesToken(header: string | string[] | undefined, expectedToken: string): boolean {
|
||||||
|
if (typeof header === 'string') {
|
||||||
|
return header === expectedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(header)) {
|
||||||
|
return header.includes(expectedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorizationMatchesToken(
|
||||||
|
header: string | string[] | undefined,
|
||||||
|
expectedToken: string
|
||||||
|
): boolean {
|
||||||
|
const values = Array.isArray(header) ? header : header ? [header] : [];
|
||||||
|
return values.some((value) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed === expectedToken) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /^Bearer\s+(.+)$/i.exec(trimmed);
|
||||||
|
return match?.[1] === expectedToken;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function hasValidDaemonToken(req: http.IncomingMessage): boolean {
|
function hasValidDaemonToken(req: http.IncomingMessage): boolean {
|
||||||
const expectedToken = process.env.CCS_CURSOR_DAEMON_TOKEN;
|
const expectedToken = process.env.CCS_CURSOR_DAEMON_TOKEN;
|
||||||
if (!expectedToken) {
|
if (!expectedToken) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provided = req.headers['x-ccs-cursor-token'];
|
return (
|
||||||
if (typeof provided === 'string') {
|
headerMatchesToken(req.headers['x-ccs-cursor-token'], expectedToken) ||
|
||||||
return provided === expectedToken;
|
headerMatchesToken(req.headers['anthropic-auth-token'], expectedToken) ||
|
||||||
}
|
headerMatchesToken(req.headers['x-api-key'], expectedToken) ||
|
||||||
|
authorizationMatchesToken(req.headers.authorization, expectedToken)
|
||||||
if (Array.isArray(provided)) {
|
);
|
||||||
return provided.includes(expectedToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInboundRequestId(req: http.IncomingMessage): string | undefined {
|
function resolveInboundRequestId(req: http.IncomingMessage): string | undefined {
|
||||||
@@ -180,17 +205,20 @@ function withCursorDaemonRequestContext<T>(
|
|||||||
|
|
||||||
function normalizeMessages(raw: unknown): NormalizedOpenAIMessage[] {
|
function normalizeMessages(raw: unknown): NormalizedOpenAIMessage[] {
|
||||||
if (!Array.isArray(raw)) {
|
if (!Array.isArray(raw)) {
|
||||||
throw new Error('messages must be an array');
|
throw new ValidationError('messages must be an array', 'messages');
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw.map((message, index) => {
|
return raw.map((message, index) => {
|
||||||
if (typeof message !== 'object' || message === null) {
|
if (typeof message !== 'object' || message === null) {
|
||||||
throw new Error(`messages[${index}] must be an object`);
|
throw new ValidationError(`messages[${index}] must be an object`, `messages[${index}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = message as Record<string, unknown>;
|
const m = message as Record<string, unknown>;
|
||||||
if (typeof m.role !== 'string' || !m.role) {
|
if (typeof m.role !== 'string' || !m.role) {
|
||||||
throw new Error(`messages[${index}].role must be a non-empty string`);
|
throw new ValidationError(
|
||||||
|
`messages[${index}].role must be a non-empty string`,
|
||||||
|
`messages[${index}].role`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = m.content;
|
const content = m.content;
|
||||||
@@ -200,7 +228,10 @@ function normalizeMessages(raw: unknown): NormalizedOpenAIMessage[] {
|
|||||||
typeof content !== 'string' &&
|
typeof content !== 'string' &&
|
||||||
!Array.isArray(content)
|
!Array.isArray(content)
|
||||||
) {
|
) {
|
||||||
throw new Error(`messages[${index}].content must be string, array, or null`);
|
throw new ValidationError(
|
||||||
|
`messages[${index}].content must be string, array, or null`,
|
||||||
|
`messages[${index}].content`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -364,7 +395,11 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAnthropicRoute) {
|
if (isAnthropicRoute) {
|
||||||
const expectedToken = (process.env.ANTHROPIC_AUTH_TOKEN || 'cursor-managed').trim();
|
const expectedToken = (
|
||||||
|
process.env.ANTHROPIC_AUTH_TOKEN ||
|
||||||
|
process.env.CCS_CURSOR_DAEMON_TOKEN ||
|
||||||
|
'cursor-managed'
|
||||||
|
).trim();
|
||||||
const requestToken = getAnthropicRequestToken(req.headers);
|
const requestToken = getAnthropicRequestToken(req.headers);
|
||||||
if (!expectedToken || requestToken !== expectedToken) {
|
if (!expectedToken || requestToken !== expectedToken) {
|
||||||
await pipeWebResponseToNode(
|
await pipeWebResponseToNode(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import type { CursorDaemonConfig, CursorDaemonStatus } from './types';
|
import type { CursorDaemonConfig, CursorDaemonStatus } from './types';
|
||||||
import { getPidFromFile, writePidToFile, removePidFile } from './cursor-daemon-pid';
|
import { getPidFromFile, writePidToFile, removePidFile } from './cursor-daemon-pid';
|
||||||
|
import { getCursorDaemonToken } from './cursor-daemon-auth';
|
||||||
import { verifyDaemonOwnership } from './daemon-process-ownership';
|
import { verifyDaemonOwnership } from './daemon-process-ownership';
|
||||||
import { createLogger, forwardRequestIdEnv } from '../services/logging';
|
import { createLogger, forwardRequestIdEnv } from '../services/logging';
|
||||||
export { getPidFromFile, writePidToFile, removePidFile } from './cursor-daemon-pid';
|
export { getPidFromFile, writePidToFile, removePidFile } from './cursor-daemon-pid';
|
||||||
@@ -53,6 +54,15 @@ async function resolveDaemonEntrypoint(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildDaemonProcessEnv(daemonToken: string): NodeJS.ProcessEnv {
|
||||||
|
return {
|
||||||
|
...process.env,
|
||||||
|
...forwardRequestIdEnv(),
|
||||||
|
CCS_CURSOR_DAEMON_TOKEN: daemonToken,
|
||||||
|
ANTHROPIC_AUTH_TOKEN: daemonToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if cursor daemon is running on the specified port.
|
* Check if cursor daemon is running on the specified port.
|
||||||
* Uses 127.0.0.1 instead of localhost for more reliable local connections.
|
* Uses 127.0.0.1 instead of localhost for more reliable local connections.
|
||||||
@@ -108,8 +118,11 @@ export async function isDaemonRunning(port: number, daemonToken?: string): Promi
|
|||||||
/**
|
/**
|
||||||
* Get daemon status.
|
* Get daemon status.
|
||||||
*/
|
*/
|
||||||
export async function getDaemonStatus(port: number): Promise<CursorDaemonStatus> {
|
export async function getDaemonStatus(
|
||||||
const running = await isDaemonRunning(port);
|
port: number,
|
||||||
|
daemonToken = getCursorDaemonToken()
|
||||||
|
): Promise<CursorDaemonStatus> {
|
||||||
|
const running = await isDaemonRunning(port, daemonToken);
|
||||||
const pid = getPidFromFile();
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -224,11 +237,7 @@ export async function startDaemon(
|
|||||||
proc = spawn(process.execPath, args, {
|
proc = spawn(process.execPath, args, {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
detached: true,
|
detached: true,
|
||||||
env: {
|
env: buildDaemonProcessEnv(effectiveConfig.daemon_token || ''),
|
||||||
...process.env,
|
|
||||||
...forwardRequestIdEnv(),
|
|
||||||
CCS_CURSOR_DAEMON_TOKEN: effectiveConfig.daemon_token || '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Unref so parent can exit
|
// Unref so parent can exit
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export async function probeCursorRuntime(config: CursorConfig): Promise<CursorPr
|
|||||||
const startResult = await startDaemon({
|
const startResult = await startDaemon({
|
||||||
port: config.port,
|
port: config.port,
|
||||||
ghost_mode: config.ghost_mode,
|
ghost_mode: config.ghost_mode,
|
||||||
|
daemon_token: daemonToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!startResult.success) {
|
if (!startResult.success) {
|
||||||
@@ -190,7 +191,7 @@ export async function probeCursorRuntime(config: CursorConfig): Promise<CursorPr
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:${config.port}/v1/chat/completions`, {
|
const response = await fetch(`http://127.0.0.1:${config.port}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'x-ccs-cursor-token': daemonToken },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model,
|
model,
|
||||||
max_tokens: 8,
|
max_tokens: 8,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '../../cursor';
|
} from '../../cursor';
|
||||||
|
|
||||||
import cursorSettingsRoutes from './cursor-settings-routes';
|
import cursorSettingsRoutes from './cursor-settings-routes';
|
||||||
|
import { getCursorDaemonToken } from '../../cursor/cursor-daemon-auth';
|
||||||
import { getCursorConfig } from '../../config/config-loader-facade';
|
import { getCursorConfig } from '../../config/config-loader-facade';
|
||||||
import { isDashboardWebSocketOriginAllowed } from '../middleware/auth-middleware';
|
import { isDashboardWebSocketOriginAllowed } from '../middleware/auth-middleware';
|
||||||
|
|
||||||
@@ -233,6 +234,7 @@ router.post('/daemon/start', async (_req: Request, res: Response): Promise<void>
|
|||||||
const result = await startDaemon({
|
const result = await startDaemon({
|
||||||
port: cursorConfig.port,
|
port: cursorConfig.port,
|
||||||
ghost_mode: cursorConfig.ghost_mode,
|
ghost_mode: cursorConfig.ghost_mode,
|
||||||
|
daemon_token: getCursorDaemonToken(),
|
||||||
});
|
});
|
||||||
const { daemonToken: _redactedDaemonToken, ...publicResult } = result;
|
const { daemonToken: _redactedDaemonToken, ...publicResult } = result;
|
||||||
void _redactedDaemonToken;
|
void _redactedDaemonToken;
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ describe('cursor daemon lifecycle smoke', () => {
|
|||||||
|
|
||||||
expect(await isDaemonRunning(port, daemonToken)).toBe(true);
|
expect(await isDaemonRunning(port, daemonToken)).toBe(true);
|
||||||
|
|
||||||
|
const standardAuthHealthResponse = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||||
|
headers: { 'x-api-key': daemonToken },
|
||||||
|
});
|
||||||
|
expect(standardAuthHealthResponse.status).toBe(200);
|
||||||
|
|
||||||
|
const bearerAuthHealthResponse = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||||
|
headers: { Authorization: `Bearer ${daemonToken}` },
|
||||||
|
});
|
||||||
|
expect(bearerAuthHealthResponse.status).toBe(200);
|
||||||
|
|
||||||
const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`);
|
const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`);
|
||||||
expect(modelsResponse.status).toBe(200);
|
expect(modelsResponse.status).toBe(200);
|
||||||
const modelsJson = (await modelsResponse.json()) as { object?: string; data?: unknown[] };
|
const modelsJson = (await modelsResponse.json()) as { object?: string; data?: unknown[] };
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ import {
|
|||||||
getDaemonStatus,
|
getDaemonStatus,
|
||||||
stopDaemon,
|
stopDaemon,
|
||||||
startDaemon,
|
startDaemon,
|
||||||
|
buildDaemonProcessEnv,
|
||||||
} from '../../../src/cursor/cursor-daemon';
|
} from '../../../src/cursor/cursor-daemon';
|
||||||
import { getCcsDir } from '../../../src/utils/config-manager';
|
import { getCcsDir } from '../../../src/utils/config-manager';
|
||||||
import { handleCursorCommand } from '../../../src/commands/cursor-command';
|
import { handleCursorCommand } from '../../../src/commands/cursor-command';
|
||||||
import {
|
import { renderCursorHelp, renderCursorStatus } from '../../../src/commands/cursor-command-display';
|
||||||
renderCursorHelp,
|
|
||||||
renderCursorStatus,
|
|
||||||
} from '../../../src/commands/cursor-command-display';
|
|
||||||
import { loadCredentials } from '../../../src/cursor/cursor-auth';
|
import { loadCredentials } from '../../../src/cursor/cursor-auth';
|
||||||
|
import { getCursorDaemonToken } from '../../../src/cursor/cursor-daemon-auth';
|
||||||
import { DEFAULT_CURSOR_CONFIG } from '../../../src/config/unified-config-types';
|
import { DEFAULT_CURSOR_CONFIG } from '../../../src/config/unified-config-types';
|
||||||
|
|
||||||
// Test isolation
|
// Test isolation
|
||||||
@@ -148,6 +147,26 @@ describe('removePidFile', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildDaemonProcessEnv', () => {
|
||||||
|
it('uses the daemon token for both daemon and Anthropic caller auth', () => {
|
||||||
|
const originalAnthropicToken = process.env.ANTHROPIC_AUTH_TOKEN;
|
||||||
|
process.env.ANTHROPIC_AUTH_TOKEN = 'inherited-stale-token';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const env = buildDaemonProcessEnv('generated-daemon-token');
|
||||||
|
|
||||||
|
expect(env.CCS_CURSOR_DAEMON_TOKEN).toBe('generated-daemon-token');
|
||||||
|
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('generated-daemon-token');
|
||||||
|
} finally {
|
||||||
|
if (originalAnthropicToken !== undefined) {
|
||||||
|
process.env.ANTHROPIC_AUTH_TOKEN = originalAnthropicToken;
|
||||||
|
} else {
|
||||||
|
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('startDaemon', () => {
|
describe('startDaemon', () => {
|
||||||
it('rejects invalid port (0)', async () => {
|
it('rejects invalid port (0)', async () => {
|
||||||
const result = await startDaemon({ port: 0 });
|
const result = await startDaemon({ port: 0 });
|
||||||
@@ -197,7 +216,7 @@ describe('isDaemonRunning', () => {
|
|||||||
throw new Error('Unable to resolve test server port');
|
throw new Error('Unable to resolve test server port');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await isDaemonRunning(address.port, "bad-token");
|
const result = await isDaemonRunning(address.port, 'bad-token');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@@ -222,6 +241,41 @@ describe('getDaemonStatus', () => {
|
|||||||
expect(status.port).toBe(19999);
|
expect(status.port).toBe(19999);
|
||||||
expect(status.pid).toBeUndefined();
|
expect(status.pid).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the persisted daemon token when checking status', async () => {
|
||||||
|
const daemonToken = getCursorDaemonToken();
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === '/health' && req.headers['x-ccs-cursor-token'] === daemonToken) {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: true, service: 'cursor-daemon' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server.listen(0, '127.0.0.1', () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Unable to resolve test server port');
|
||||||
|
}
|
||||||
|
|
||||||
|
writePidToFile(12345);
|
||||||
|
const status = await getDaemonStatus(address.port);
|
||||||
|
expect(status.running).toBe(true);
|
||||||
|
expect(status.port).toBe(address.port);
|
||||||
|
expect(status.pid).toBe(12345);
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server.close(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('stopDaemon', () => {
|
describe('stopDaemon', () => {
|
||||||
@@ -275,8 +329,10 @@ describe('stopDaemon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('refuses to stop when daemon ownership cannot be verified', async () => {
|
it('refuses to stop when daemon ownership cannot be verified', async () => {
|
||||||
const killSpy = spyOn(process, 'kill').mockImplementation(
|
const killSpy = spyOn(process, 'kill').mockImplementation(((
|
||||||
((pid: number, signal?: NodeJS.Signals | number) => {
|
pid: number,
|
||||||
|
signal?: NodeJS.Signals | number
|
||||||
|
) => {
|
||||||
if (pid === process.pid && signal === 0) {
|
if (pid === process.pid && signal === 0) {
|
||||||
const err = new Error('EPERM') as NodeJS.ErrnoException;
|
const err = new Error('EPERM') as NodeJS.ErrnoException;
|
||||||
err.code = 'EPERM';
|
err.code = 'EPERM';
|
||||||
@@ -284,8 +340,7 @@ describe('stopDaemon', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}) as typeof process.kill
|
}) as typeof process.kill);
|
||||||
);
|
|
||||||
|
|
||||||
writePidToFile(process.pid);
|
writePidToFile(process.pid);
|
||||||
|
|
||||||
@@ -388,11 +443,13 @@ describe('renderCursorStatus', () => {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
logs.some((line) => line.includes('Chat route: http://127.0.0.1:20129/v1/chat/completions'))
|
logs.some((line) =>
|
||||||
).toBe(true);
|
line.includes('Chat route: http://127.0.0.1:20129/v1/chat/completions')
|
||||||
expect(
|
)
|
||||||
logs.some((line) => line.includes('Anthropic base: http://127.0.0.1:20129'))
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes('Anthropic base: http://127.0.0.1:20129'))).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
logs.some((line) => line.includes(`Raw settings: ${getCcsDir()}/cursor.settings.json`))
|
logs.some((line) => line.includes(`Raw settings: ${getCcsDir()}/cursor.settings.json`))
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -460,9 +517,9 @@ describe('renderCursorStatus', () => {
|
|||||||
{ running: true, port: 20129, pid: 1234 }
|
{ running: true, port: 20129, pid: 1234 }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(logs.some((line) => line.includes('Raw settings: ~/.ccs/cursor.settings.json'))).toBe(
|
expect(
|
||||||
true
|
logs.some((line) => line.includes('Raw settings: ~/.ccs/cursor.settings.json'))
|
||||||
);
|
).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
if (originalCcsHomeValue !== undefined) {
|
if (originalCcsHomeValue !== undefined) {
|
||||||
process.env.CCS_HOME = originalCcsHomeValue;
|
process.env.CCS_HOME = originalCcsHomeValue;
|
||||||
@@ -496,15 +553,11 @@ describe('renderCursorHelp', () => {
|
|||||||
line.includes('Deprecated: `ccs cursor` now belongs to the CLIProxy Cursor provider.')
|
line.includes('Deprecated: `ccs cursor` now belongs to the CLIProxy Cursor provider.')
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))).toBe(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
logs.some((line) => line.includes('ccs cursor --auth'))
|
logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
logs.some((line) => line.includes('ccs legacy cursor [claude args]'))
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes('ccs cursor --auth'))).toBe(true);
|
||||||
|
expect(logs.some((line) => line.includes('ccs legacy cursor [claude args]'))).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
console.log = originalLog;
|
console.log = originalLog;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user