Merge branch 'main' into feature/webui
This commit is contained in:
31
README.md
31
README.md
@@ -102,6 +102,12 @@ antigravity-claude-proxy accounts add --no-browser
|
|||||||
|
|
||||||
If you have the **Antigravity** app installed and logged in, the proxy will automatically detect your local session. No additional setup is required.
|
If you have the **Antigravity** app installed and logged in, the proxy will automatically detect your local session. No additional setup is required.
|
||||||
|
|
||||||
|
To use a custom port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=3001 antigravity-claude-proxy start
|
||||||
|
```
|
||||||
|
|
||||||
### 3. Verify It's Working
|
### 3. Verify It's Working
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -215,6 +221,31 @@ claude
|
|||||||
|
|
||||||
> **Note:** If Claude Code asks you to select a login method, add `"hasCompletedOnboarding": true` to `~/.claude.json` (macOS/Linux) or `%USERPROFILE%\.claude.json` (Windows), then restart your terminal and try again.
|
> **Note:** If Claude Code asks you to select a login method, add `"hasCompletedOnboarding": true` to `~/.claude.json` (macOS/Linux) or `%USERPROFILE%\.claude.json` (Windows), then restart your terminal and try again.
|
||||||
|
|
||||||
|
### Multiple Claude Code Instances (Optional)
|
||||||
|
|
||||||
|
To run both the official Claude Code and Antigravity version simultaneously, add this alias:
|
||||||
|
|
||||||
|
**macOS / Linux:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add to ~/.zshrc or ~/.bashrc
|
||||||
|
alias claude-antigravity='CLAUDE_CONFIG_DIR=~/.claude-account-antigravity ANTHROPIC_BASE_URL="http://localhost:8080" ANTHROPIC_AUTH_TOKEN="test" command claude'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Add to $PROFILE
|
||||||
|
function claude-antigravity {
|
||||||
|
$env:CLAUDE_CONFIG_DIR = "$env:USERPROFILE\.claude-account-antigravity"
|
||||||
|
$env:ANTHROPIC_BASE_URL = "http://localhost:8080"
|
||||||
|
$env:ANTHROPIC_AUTH_TOKEN = "test"
|
||||||
|
claude
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `claude` for official API or `claude-antigravity` for this proxy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Available Models
|
## Available Models
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
"test:caching": "node tests/test-caching-streaming.cjs",
|
"test:caching": "node tests/test-caching-streaming.cjs",
|
||||||
"test:crossmodel": "node tests/test-cross-model-thinking.cjs",
|
"test:crossmodel": "node tests/test-cross-model-thinking.cjs",
|
||||||
"test:oauth": "node tests/test-oauth-no-browser.cjs",
|
"test:oauth": "node tests/test-oauth-no-browser.cjs",
|
||||||
"test:emptyretry": "node tests/test-empty-response-retry.cjs"
|
"test:emptyretry": "node tests/test-empty-response-retry.cjs",
|
||||||
|
"test:sanitizer": "node tests/test-schema-sanitizer.cjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
isThinkingModel
|
isThinkingModel
|
||||||
} from '../constants.js';
|
} from '../constants.js';
|
||||||
import { convertContentToParts, convertRole } from './content-converter.js';
|
import { convertContentToParts, convertRole } from './content-converter.js';
|
||||||
import { sanitizeSchema, cleanSchemaForGemini } from './schema-sanitizer.js';
|
import { sanitizeSchema, cleanSchema } from './schema-sanitizer.js';
|
||||||
import {
|
import {
|
||||||
restoreThinkingSignatures,
|
restoreThinkingSignatures,
|
||||||
removeTrailingThinkingBlocks,
|
removeTrailingThinkingBlocks,
|
||||||
@@ -210,10 +210,11 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
|||||||
// Sanitize schema for general compatibility
|
// Sanitize schema for general compatibility
|
||||||
let parameters = sanitizeSchema(schema);
|
let parameters = sanitizeSchema(schema);
|
||||||
|
|
||||||
// For Gemini models, apply additional cleaning for VALIDATED mode
|
// Apply Google-format cleaning for ALL models since they all go through
|
||||||
if (isGeminiModel) {
|
// Cloud Code API which validates schemas using Google's protobuf format.
|
||||||
parameters = cleanSchemaForGemini(parameters);
|
// This fixes issue #82: /compact command fails with schema transformation error
|
||||||
}
|
// "Proto field is not repeating, cannot start list" for Claude models.
|
||||||
|
parameters = cleanSchema(parameters);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
|
name: String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64),
|
||||||
|
|||||||
@@ -564,6 +564,27 @@ export function sanitizeSchema(schema) {
|
|||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert JSON Schema type names to Google's Protobuf-style uppercase type names.
|
||||||
|
* Google's Generative AI API expects uppercase types: STRING, OBJECT, ARRAY, etc.
|
||||||
|
*
|
||||||
|
* @param {string} type - JSON Schema type name (lowercase)
|
||||||
|
* @returns {string} Google-format type name (uppercase)
|
||||||
|
*/
|
||||||
|
function toGoogleType(type) {
|
||||||
|
if (!type || typeof type !== 'string') return type;
|
||||||
|
const typeMap = {
|
||||||
|
'string': 'STRING',
|
||||||
|
'number': 'NUMBER',
|
||||||
|
'integer': 'INTEGER',
|
||||||
|
'boolean': 'BOOLEAN',
|
||||||
|
'array': 'ARRAY',
|
||||||
|
'object': 'OBJECT',
|
||||||
|
'null': 'STRING' // Fallback for null type
|
||||||
|
};
|
||||||
|
return typeMap[type.toLowerCase()] || type.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans JSON schema for Gemini API compatibility.
|
* Cleans JSON schema for Gemini API compatibility.
|
||||||
* Uses a multi-phase pipeline matching opencode-antigravity-auth approach.
|
* Uses a multi-phase pipeline matching opencode-antigravity-auth approach.
|
||||||
@@ -571,9 +592,9 @@ export function sanitizeSchema(schema) {
|
|||||||
* @param {Object} schema - The JSON schema to clean
|
* @param {Object} schema - The JSON schema to clean
|
||||||
* @returns {Object} Cleaned schema safe for Gemini API
|
* @returns {Object} Cleaned schema safe for Gemini API
|
||||||
*/
|
*/
|
||||||
export function cleanSchemaForGemini(schema) {
|
export function cleanSchema(schema) {
|
||||||
if (!schema || typeof schema !== 'object') return schema;
|
if (!schema || typeof schema !== 'object') return schema;
|
||||||
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
if (Array.isArray(schema)) return schema.map(cleanSchema);
|
||||||
|
|
||||||
// Phase 1: Convert $refs to hints
|
// Phase 1: Convert $refs to hints
|
||||||
let result = convertRefsToHints(schema);
|
let result = convertRefsToHints(schema);
|
||||||
@@ -620,16 +641,16 @@ export function cleanSchemaForGemini(schema) {
|
|||||||
if (result.properties && typeof result.properties === 'object') {
|
if (result.properties && typeof result.properties === 'object') {
|
||||||
const newProps = {};
|
const newProps = {};
|
||||||
for (const [key, value] of Object.entries(result.properties)) {
|
for (const [key, value] of Object.entries(result.properties)) {
|
||||||
newProps[key] = cleanSchemaForGemini(value);
|
newProps[key] = cleanSchema(value);
|
||||||
}
|
}
|
||||||
result.properties = newProps;
|
result.properties = newProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.items) {
|
if (result.items) {
|
||||||
if (Array.isArray(result.items)) {
|
if (Array.isArray(result.items)) {
|
||||||
result.items = result.items.map(cleanSchemaForGemini);
|
result.items = result.items.map(cleanSchema);
|
||||||
} else if (typeof result.items === 'object') {
|
} else if (typeof result.items === 'object') {
|
||||||
result.items = cleanSchemaForGemini(result.items);
|
result.items = cleanSchema(result.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,5 +663,11 @@ export function cleanSchemaForGemini(schema) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 5: Convert type to Google's uppercase format (STRING, OBJECT, ARRAY, etc.)
|
||||||
|
// Only convert at current level - nested types already converted by recursive cleanSchema calls
|
||||||
|
if (result.type && typeof result.type === 'string') {
|
||||||
|
result.type = toGoogleType(result.type);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const tests = [
|
|||||||
{ name: 'Prompt Caching', file: 'test-caching-streaming.cjs' },
|
{ name: 'Prompt Caching', file: 'test-caching-streaming.cjs' },
|
||||||
{ name: 'Cross-Model Thinking', file: 'test-cross-model-thinking.cjs' },
|
{ name: 'Cross-Model Thinking', file: 'test-cross-model-thinking.cjs' },
|
||||||
{ name: 'OAuth No-Browser Mode', file: 'test-oauth-no-browser.cjs' },
|
{ name: 'OAuth No-Browser Mode', file: 'test-oauth-no-browser.cjs' },
|
||||||
{ name: 'Empty Response Retry', file: 'test-empty-response-retry.cjs' }
|
{ name: 'Empty Response Retry', file: 'test-empty-response-retry.cjs' },
|
||||||
|
{ name: 'Schema Sanitizer', file: 'test-schema-sanitizer.cjs' }
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runTest(test) {
|
async function runTest(test) {
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ Please do this step by step, reading each file before modifying.`
|
|||||||
if (!passed) allPassed = false;
|
if (!passed) allPassed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== TEST 2: Multiple tool calls in sequence =====
|
// ===== TEST 2: Response after tool result =====
|
||||||
console.log('\nTEST 2: Tool result followed by more thinking');
|
console.log('\nTEST 2: Response after tool result');
|
||||||
console.log('-'.repeat(40));
|
console.log('-'.repeat(40));
|
||||||
|
|
||||||
// Start with previous result and add tool result
|
// Start with previous result and add tool result
|
||||||
@@ -141,14 +141,16 @@ Please do this step by step, reading each file before modifying.`
|
|||||||
console.log(` Response: "${text2[0].text?.substring(0, 80)}..."`);
|
console.log(` Response: "${text2[0].text?.substring(0, 80)}..."`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should have thinking after receiving tool result
|
// Model may or may not produce thinking blocks after tool result
|
||||||
const passed = thinking2.length >= 1 && (text2.length > 0 || toolUse2.length > 0);
|
// The key is that it produces a valid response (text or tool use)
|
||||||
results.push({ name: 'Thinking after tool result', passed });
|
// Note: Thinking is optional - model decides when to use it based on task complexity
|
||||||
|
const passed = text2.length > 0 || toolUse2.length > 0;
|
||||||
|
results.push({ name: 'Response after tool result', passed });
|
||||||
if (!passed) allPassed = false;
|
if (!passed) allPassed = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(' SKIPPED - No tool use in previous test');
|
console.log(' SKIPPED - No tool use in previous test');
|
||||||
results.push({ name: 'Thinking after tool result', passed: false, skipped: true });
|
results.push({ name: 'Response after tool result', passed: false, skipped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Summary =====
|
// ===== Summary =====
|
||||||
|
|||||||
269
tests/test-schema-sanitizer.cjs
Normal file
269
tests/test-schema-sanitizer.cjs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* Test Schema Sanitizer - Tests for schema transformation for Google API
|
||||||
|
*
|
||||||
|
* Verifies that complex nested array schemas are properly converted to
|
||||||
|
* Google's protobuf format (uppercase type names) to fix issue #82:
|
||||||
|
* "Proto field is not repeating, cannot start list"
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import the schema sanitizer functions
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Since we're in CommonJS and the module is ESM, we need to use dynamic import
|
||||||
|
async function runTests() {
|
||||||
|
console.log('╔══════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ SCHEMA SANITIZER TEST SUITE ║');
|
||||||
|
console.log('╚══════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
// Dynamic import for ESM module
|
||||||
|
const { sanitizeSchema, cleanSchema } = await import('../src/format/schema-sanitizer.js');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(`✓ ${name}`);
|
||||||
|
passed++;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`✗ ${name}`);
|
||||||
|
console.log(` Error: ${e.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEqual(actual, expected, message = '') {
|
||||||
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||||
|
throw new Error(`${message}\nExpected: ${JSON.stringify(expected, null, 2)}\nActual: ${JSON.stringify(actual, null, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIncludes(actual, substring, message = '') {
|
||||||
|
if (!JSON.stringify(actual).includes(substring)) {
|
||||||
|
throw new Error(`${message}\nExpected to include: ${substring}\nActual: ${JSON.stringify(actual, null, 2)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: Basic type conversion to uppercase
|
||||||
|
test('Basic type conversion to uppercase', () => {
|
||||||
|
const schema = { type: 'string', description: 'A test string' };
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
assertEqual(result.type, 'STRING', 'Type should be uppercase STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Object type conversion
|
||||||
|
test('Object type conversion', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
age: { type: 'integer' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
assertEqual(result.type, 'OBJECT', 'Object type should be uppercase');
|
||||||
|
assertEqual(result.properties.name.type, 'STRING', 'Nested string type should be uppercase');
|
||||||
|
assertEqual(result.properties.age.type, 'INTEGER', 'Nested integer type should be uppercase');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Array type conversion (the main bug fix)
|
||||||
|
test('Array type conversion with items', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
assertEqual(result.type, 'ARRAY', 'Array type should be uppercase ARRAY');
|
||||||
|
assertEqual(result.items.type, 'STRING', 'Items type should be uppercase STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Nested array inside object (the actual bug scenario)
|
||||||
|
test('Nested array inside object (Claude Code TodoWrite-style schema)', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
todos: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'integer' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
status: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
|
||||||
|
assertEqual(result.type, 'OBJECT', 'Root type should be OBJECT');
|
||||||
|
assertEqual(result.properties.todos.type, 'ARRAY', 'Todos type should be ARRAY');
|
||||||
|
assertEqual(result.properties.todos.items.type, 'OBJECT', 'Items type should be OBJECT');
|
||||||
|
assertEqual(result.properties.todos.items.properties.id.type, 'INTEGER', 'id type should be INTEGER');
|
||||||
|
assertEqual(result.properties.todos.items.properties.title.type, 'STRING', 'title type should be STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Complex nested structure (simulating Claude Code tools)
|
||||||
|
test('Complex nested structure with multiple array levels', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
tasks: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
subtasks: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
step: { type: 'string' },
|
||||||
|
completed: { type: 'boolean' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
count: { type: 'number' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
|
||||||
|
assertEqual(result.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.tasks.type, 'ARRAY');
|
||||||
|
assertEqual(result.properties.tasks.items.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.tasks.items.properties.subtasks.type, 'ARRAY');
|
||||||
|
assertEqual(result.properties.tasks.items.properties.subtasks.items.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.tasks.items.properties.subtasks.items.properties.completed.type, 'BOOLEAN');
|
||||||
|
assertEqual(result.properties.count.type, 'NUMBER');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: cleanSchema handles anyOf (when not stripped by sanitizeSchema)
|
||||||
|
test('cleanSchema handles anyOf and converts types', () => {
|
||||||
|
// Test cleanSchema directly with anyOf (bypassing sanitizeSchema)
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
anyOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{ type: 'object', properties: { name: { type: 'string' } } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(schema);
|
||||||
|
|
||||||
|
assertEqual(result.type, 'OBJECT');
|
||||||
|
// anyOf gets flattened to best option (object type scores highest)
|
||||||
|
assertEqual(result.properties.value.type, 'OBJECT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Schema with type array (nullable)
|
||||||
|
test('Schema with type array (nullable) gets flattened and converted', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
optional: {
|
||||||
|
type: ['string', 'null']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
|
||||||
|
assertEqual(result.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.optional.type, 'STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: All primitive types
|
||||||
|
test('All primitive types converted correctly', () => {
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
str: { type: 'string' },
|
||||||
|
num: { type: 'number' },
|
||||||
|
int: { type: 'integer' },
|
||||||
|
bool: { type: 'boolean' },
|
||||||
|
arr: { type: 'array', items: { type: 'string' } },
|
||||||
|
obj: { type: 'object', properties: { x: { type: 'string' } } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
|
||||||
|
assertEqual(result.properties.str.type, 'STRING');
|
||||||
|
assertEqual(result.properties.num.type, 'NUMBER');
|
||||||
|
assertEqual(result.properties.int.type, 'INTEGER');
|
||||||
|
assertEqual(result.properties.bool.type, 'BOOLEAN');
|
||||||
|
assertEqual(result.properties.arr.type, 'ARRAY');
|
||||||
|
assertEqual(result.properties.obj.type, 'OBJECT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 9: Empty schema gets placeholder with correct types
|
||||||
|
test('Empty schema gets placeholder with uppercase types', () => {
|
||||||
|
const result = cleanSchema(sanitizeSchema(null));
|
||||||
|
|
||||||
|
assertEqual(result.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.reason.type, 'STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 10: Real-world Claude Code tool schema simulation
|
||||||
|
test('Real-world Claude Code ManageTodoList-style schema', () => {
|
||||||
|
// Simulates the type of schema that caused issue #82
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
operation: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['write', 'read']
|
||||||
|
},
|
||||||
|
todoList: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'number' },
|
||||||
|
title: { type: 'string' },
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['not-started', 'in-progress', 'completed']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['id', 'title', 'status']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['operation']
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = cleanSchema(sanitizeSchema(schema));
|
||||||
|
|
||||||
|
// Verify all types are uppercase
|
||||||
|
assertEqual(result.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.operation.type, 'STRING');
|
||||||
|
assertEqual(result.properties.todoList.type, 'ARRAY');
|
||||||
|
assertEqual(result.properties.todoList.items.type, 'OBJECT');
|
||||||
|
assertEqual(result.properties.todoList.items.properties.id.type, 'NUMBER');
|
||||||
|
assertEqual(result.properties.todoList.items.properties.title.type, 'STRING');
|
||||||
|
assertEqual(result.properties.todoList.items.properties.status.type, 'STRING');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n' + '═'.repeat(60));
|
||||||
|
console.log(`Tests completed: ${passed} passed, ${failed} failed`);
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests().catch(err => {
|
||||||
|
console.error('Test suite failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user