Skip to content

Commit

Permalink
Bug Fix: api error
Browse files Browse the repository at this point in the history
  • Loading branch information
Talamantez committed Nov 24, 2024
1 parent d4585ab commit 789a1dd
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 23 deletions.
25 changes: 16 additions & 9 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
.vscode/**
.vscode-test/**
.pnpm-store/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
webpack.config.js
.npmrc
pnpm-lock.yaml
esbuild.js
vsc-extension-quickstart.md
/tsconfig.json
/.eslintrc.json
/.map
**/.ts
.github/
examples/
.devcontainer/
test/**
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
.github/**
examples/**
.devcontainer/**
test/**
dist/test/**
.eslint*
coverage/**
.nyc_output/**
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2024-11-24
### Fixed
API Endpoint 400 error
### Changed
Update API Test Suite

## [2.0.0] - 2024-11-24
## [2.0.1] - 2024-11-24
### Fixed
Purchase link

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "claude-vscode-assistant",
"displayName": "Claude AI Assistant",
"version": "2.0.1",
"version": "2.1.0",
"description": "Claude AI assistant for Visual Studio Code - Seamlessly integrate Claude's capabilities into your development workflow",
"publisher": "conscious-robot",
"pricing": "Trial",
Expand Down
23 changes: 11 additions & 12 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import * as vscode from 'vscode';
import { getConfiguration } from './config';

// Constants
// Constants and type definitions
const SERVICE_URL = 'https://api.anthropic.com/v1/messages';
const VALID_MODELS = ['claude-3-opus-20240229', 'claude-3-sonnet-20240229'] as const;
type ValidModel = typeof VALID_MODELS[number];

// Interface definitions
export interface ClaudeMessageContent {
type: 'text'; // Restrict to known types
text: string;
Expand All @@ -28,7 +29,7 @@ export interface ClaudeResponse {
dailyLimit?: number;
}

// Enhanced type guard with complete validation
// Type guard functions - defined before use
function isClaudeMessageContent(item: unknown): item is ClaudeMessageContent {
return (
typeof item === 'object' &&
Expand All @@ -43,30 +44,25 @@ function isClaudeMessageContent(item: unknown): item is ClaudeMessageContent {
function isClaudeResponse(data: unknown): data is ClaudeResponse {
const response = data as Partial<ClaudeResponse>;

// Basic structure check
if (typeof data !== 'object' || data === null) {
return false;
}

// Required fields check
const requiredStringFields = ['id', 'type', 'role'] as const;
for (const field of requiredStringFields) {
if (typeof response[field] !== 'string') {
return false;
}
}

// Content array check
if (!Array.isArray(response.content)) {
return false;
}

// Validate each content item
if (!response.content.every(isClaudeMessageContent)) {
return false;
}

// Usage object check
if (
typeof response.usage !== 'object' ||
response.usage === null ||
Expand All @@ -76,7 +72,6 @@ function isClaudeResponse(data: unknown): data is ClaudeResponse {
return false;
}

// Optional fields check
if (
(response.stop_reason !== null && typeof response.stop_reason !== 'string') ||
(response.stop_sequence !== null && typeof response.stop_sequence !== 'string') ||
Expand All @@ -86,14 +81,14 @@ function isClaudeResponse(data: unknown): data is ClaudeResponse {
return false;
}

// Validate model string matches expected format
if (!response.model || !VALID_MODELS.includes(response.model as ValidModel)) {
return false;
}

return true;
}

// Main API function
export async function askClaude(text: string, token?: vscode.CancellationToken): Promise<ClaudeResponse> {
const config = getConfiguration();

Expand All @@ -114,12 +109,16 @@ export async function askClaude(text: string, token?: vscode.CancellationToken):
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'anthropic-version': '2023-06-01',
'x-api-key': config.apiKey || process.env.CLAUDE_API_KEY || ''
},
body: JSON.stringify({
prompt: text,
model: config.model
messages: [{
role: 'user',
content: text
}],
model: config.model,
max_tokens: 1500
}),
signal: abortController.signal
});
Expand Down
159 changes: 159 additions & 0 deletions test/suite/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// test/suite/api.test.ts
import * as assert from 'assert';
import * as sinon from 'sinon';
import { askClaude, ClaudeResponse } from '../../src/api';
import * as vscode from 'vscode';

suite('Claude API Tests', () => {
let sandbox: sinon.SinonSandbox;
let fetchStub: sinon.SinonStub;

const mockConfig = {
apiKey: 'test-key',
model: 'claude-3-opus-20240229'
};

const mockSuccessResponse: ClaudeResponse = {
id: 'test-id',
type: 'message',
role: 'assistant',
content: [{ type: 'text', text: 'Test response' }],
model: 'claude-3-opus-20240229',
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 20
}
};

setup(() => {
sandbox = sinon.createSandbox();

// Stub global fetch
fetchStub = sandbox.stub(global, 'fetch');

// Stub configuration
sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: (key: string) => {
if (key === 'apiKey') return mockConfig.apiKey;
if (key === 'model') return mockConfig.model;
return undefined;
}
} as any);
});

teardown(() => {
sandbox.restore();
});

test('successful API call', async () => {
fetchStub.resolves({
ok: true,
json: async () => mockSuccessResponse
} as Response);

const response = await askClaude('Test prompt');

assert.strictEqual(response.content[0].text, 'Test response');
assert.strictEqual(response.model, 'claude-3-opus-20240229');

const fetchCall = fetchStub.getCall(0);
const requestInit = fetchCall.args[1] as RequestInit & {
headers: Record<string, string>;
};
const requestBody = JSON.parse(requestInit.body as string);

assert.deepStrictEqual(requestBody.messages, [{
role: 'user',
content: 'Test prompt'
}]);
assert.strictEqual(requestInit.headers['anthropic-version'], '2023-06-01');
});

test('validates request headers', async () => {
fetchStub.resolves({
ok: true,
json: async () => mockSuccessResponse
} as Response);

await askClaude('Test prompt');

const requestInit = fetchStub.getCall(0).args[1] as RequestInit & {
headers: Record<string, string>;
};

assert.deepStrictEqual(requestInit.headers, {
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
'x-api-key': 'test-key'
});
});

test('handles request cancellation', async () => {
const tokenSource = new vscode.CancellationTokenSource();

// Create an AbortError that matches the browser's native error
const abortError = new Error();
abortError.name = 'AbortError';

// Setup fetch to throw the abort error after a delay
fetchStub.callsFake(() => new Promise((_, reject) => {
setTimeout(() => {
reject(abortError);
}, 10);
}));

// Start the request and immediately cancel
const promise = askClaude('Test prompt', tokenSource.token);
tokenSource.cancel();

// Use a custom validator function
await assert.rejects(
promise,
error => {
// Check if the error is an instance of vscode.CancellationError
return error instanceof vscode.CancellationError;
},
'Expected a CancellationError'
);
});

test('handles missing API key', async () => {
// Update config stub to return no API key
sandbox.restore();
sandbox.stub(vscode.workspace, 'getConfiguration').returns({
get: (key: string) => undefined
} as any);
process.env.CLAUDE_API_KEY = '';

await assert.rejects(
askClaude('Test prompt'),
/No API key configured/
);
});

test('validates response format', async () => {
fetchStub.resolves({
ok: true,
json: async () => ({
id: 'test-id',
// Missing required fields
})
} as Response);

await assert.rejects(
askClaude('Test prompt'),
/Invalid response format/
);
});

test('handles network errors', async () => {
fetchStub.rejects(new Error('Network error'));

await assert.rejects(
askClaude('Test prompt'),
/Network error/
);
});
});

0 comments on commit 789a1dd

Please sign in to comment.