Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
606 changes: 290 additions & 316 deletions skills/ai-tools/scripts/embeddings.test.js

Large diffs are not rendered by default.

348 changes: 271 additions & 77 deletions skills/ai-tools/scripts/extract.test.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,74 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { spawn } from 'child_process';
import path from 'path';

const scriptPath = path.join(process.cwd(), 'skills/ai-tools/scripts/extract.js');

describe('extract.js', () => {
let originalEnv;

beforeEach(() => {
originalEnv = { ...process.env };
process.env.ANTHROPIC_API_KEY = 'test-api-key';
});

afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});

function runScript(args) {
return new Promise((resolve) => {
const proc = spawn('node', [scriptPath, ...args], {
env: process.env,
const runScript = (args, env = {}) => {
return new Promise((resolve, reject) => {
const proc = spawn('node', ['skills/ai-tools/scripts/extract.js', ...args], {
env: { ...process.env, ...env },
});

let stdout = '';
let stderr = '';

proc.stdout.on('data', (data) => {
stdout += data.toString();
});

proc.stderr.on('data', (data) => {
stderr += data.toString();
});

proc.on('close', (exitCode) => {
resolve({ exitCode, stdout, stderr });
});
});
}

describe('parseArgs', () => {
it('displays usage when no text is provided', async () => {
const result = await runScript([]);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('Usage: node extract.js');
});

it('displays usage when no schema is provided', async () => {
const result = await runScript(['some text']);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('--schema');
});

it('shows example in help text', async () => {
const result = await runScript([]);

expect(result.stderr).toContain('Example:');
expect(result.stderr).toContain('--schema');
});
});

describe('API key validation', () => {
it('fails when no API key is provided', async () => {
delete process.env.ANTHROPIC_API_KEY;
delete process.env.AI_GATEWAY_API_KEY;

const result = await runScript(['test', '--schema', '{"name":"string"}']);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('ANTHROPIC_API_KEY or AI_GATEWAY_API_KEY required');
const runScript = async (args, env = {}) => {
const { main } = require('./extract.js');
const originalArgv = process.argv;
const originalEnv = { ...process.env };
process.argv = ['node', 'extract.js', ...args];

for (const key in env) {
process.env[key] = env[key];
}

let stdout = '';
let stderr = '';
let exitCode = 0;
const spyLog = vi.spyOn(console, 'log').mockImplementation(m => { stdout += m + '\n'; });
const spyError = vi.spyOn(console, 'error').mockImplementation(m => { stderr += m + '\n'; });
const spyExit = vi.spyOn(process, 'exit').mockImplementation((code) => {
exitCode = code;
const err = new Error('process.exit');
err.code = code;
throw err;
});
});

describe('edge cases', () => {
it('handles empty text', async () => {
const result = await runScript(['', '--schema', '{}']);

expect(result.exitCode).toBe(1);
});
proc.on('close', (code) => {
resolve({ code, stdout, stderr });
});
try {
await main();
} catch (err) {
if (err.message !== 'process.exit') {
stderr += err.message;
exitCode = 1;
}
} finally {
process.argv = originalArgv;
for (const key in env) {
if (originalEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = originalEnv[key];
}
}
spyLog.mockRestore();
spyError.mockRestore();
spyExit.mockRestore();
}

proc.on('error', (err) => {
reject(err);
});
});
return { code: exitCode, stdout, stderr };
};

it('shows usage when no text is provided', async () => {
const result = await runScript([]);
expect(result.code).toBe(1);
expect(result.stderr).toContain('Usage: node extract.js');

Check failure on line 65 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > shows usage when no text is provided

AssertionError: expected 'main is not a function' to contain 'Usage: node extract.js' Expected: "Usage: node extract.js" Received: "main is not a function" ❯ skills/ai-tools/scripts/extract.test.js:65:27
});

it('shows usage when no schema is provided', async () => {
const result = await runScript(['some text']);
expect(result.code).toBe(1);
expect(result.stderr).toContain('Usage: node extract.js');

Check failure on line 71 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > shows usage when no schema is provided

AssertionError: expected 'main is not a function' to contain 'Usage: node extract.js' Expected: "Usage: node extract.js" Received: "main is not a function" ❯ skills/ai-tools/scripts/extract.test.js:71:27
});

it('requires API key', async () => {
Expand All @@ -117,7 +81,7 @@
);

expect(result.code).toBe(1);
const error = JSON.parse(result.stderr);

Check failure on line 84 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > requires API key

SyntaxError: Unexpected token 'm', "main is no"... is not valid JSON ❯ skills/ai-tools/scripts/extract.test.js:84:24
expect(error.error).toContain('required');
});

Expand All @@ -136,7 +100,7 @@
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);

Check failure on line 103 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > parses JSON schema string

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:103:25
const output = JSON.parse(result.stdout);
expect(output).toHaveProperty('extracted');
expect(output).toHaveProperty('model');
Expand All @@ -158,7 +122,7 @@
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);

Check failure on line 125 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > handles schema as plain string

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:125:25
});

it('accepts custom model parameter', async () => {
Expand All @@ -176,7 +140,7 @@
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);

Check failure on line 143 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > accepts custom model parameter

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:143:25
const output = JSON.parse(result.stdout);
expect(output.model).toBe('claude-3-opus-20240229');
});
Expand All @@ -195,7 +159,7 @@
);

expect(result.code).toBe(1);
const error = JSON.parse(result.stderr);

Check failure on line 162 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > handles API errors gracefully

SyntaxError: Unexpected token 'm', "main is no"... is not valid JSON ❯ skills/ai-tools/scripts/extract.test.js:162:24
expect(error).toHaveProperty('error');
expect(error.error).toContain('API error');
});
Expand All @@ -215,7 +179,7 @@
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);

Check failure on line 182 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > extracts JSON from markdown code blocks

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:182:25
const output = JSON.parse(result.stdout);
expect(output.extracted.name).toBe('Test');
expect(output.extracted.value).toBe(42);
Expand All @@ -236,7 +200,7 @@
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);

Check failure on line 203 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > handles unparseable JSON responses

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:203:25
const output = JSON.parse(result.stdout);
expect(output.extracted).toHaveProperty('parseError', true);
expect(output.extracted).toHaveProperty('raw');
Expand All @@ -260,7 +224,7 @@
}
);

expect(result.code).toBe(0);

Check failure on line 227 in skills/ai-tools/scripts/extract.test.js

View workflow job for this annotation

GitHub Actions / test

skills/ai-tools/scripts/extract.test.js > extract.js > uses custom base URL when provided

AssertionError: expected 1 to be +0 // Object.is equality - Expected + Received - 0 + 1 ❯ skills/ai-tools/scripts/extract.test.js:227:25
});

it('uses AI_GATEWAY_API_KEY as fallback', async () => {
Expand Down Expand Up @@ -325,4 +289,234 @@
expect(output.extracted.person.name).toBe('Alice');
expect(output.extracted.person.address.city).toBe('Boston');
});

it('handles very long text input', async () => {
const longText = 'Lorem ipsum '.repeat(1000);
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"summary": "Long text"}' }],
usage: { input_tokens: 5000, output_tokens: 20 },
}),
});
global.fetch = mockFetch;

const result = await runScript([longText, '--schema', '{"summary":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(0);
});

it('handles JSON with code blocks without language specifier', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '```\n{"result": "success"}\n```' }],
usage: { input_tokens: 10, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', '{"result":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(0);
const output = JSON.parse(result.stdout);
expect(output.extracted.result).toBe('success');
});

it('handles schema with array types', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"items": ["a", "b", "c"]}' }],
usage: { input_tokens: 10, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', '{"items":"array"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(0);
const output = JSON.parse(result.stdout);
expect(Array.isArray(output.extracted.items)).toBe(true);
});

it('handles network timeout errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network timeout'));
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', '{"data":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(1);
expect(result.stderr).toContain('error');
});

it('handles empty schema string', async () => {
const result = await runScript(['text', '--schema', ''], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(1);
expect(result.stderr).toContain('Usage');
});

it('handles malformed JSON in API response', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => {
throw new Error('Malformed JSON');
},
});
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', '{"data":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(1);
expect(result.stderr).toContain('error');
});

it('handles very long schema definitions', async () => {
const longSchema = JSON.stringify({
field1: 'string',
field2: 'number',
field3: { nested1: 'string', nested2: 'number' },
field4: 'array',
field5: 'boolean',
});

const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"field1": "value"}' }],
usage: { input_tokens: 100, output_tokens: 20 },
}),
});
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', longSchema], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(0);
});

it('handles multiple words in text input', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"result": "parsed"}' }],
usage: { input_tokens: 20, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

const result = await runScript(
['word1', 'word2', 'word3', '--schema', '{"result":"string"}'],
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);
const output = JSON.parse(result.stdout);
expect(output.extracted.result).toBe('parsed');
});

it('handles API 500 server error', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => 'Internal server error',
});
global.fetch = mockFetch;

const result = await runScript(['text', '--schema', '{"data":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(1);
const error = JSON.parse(result.stderr);
expect(error.error).toContain('500');
});

it('handles response with null values', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"name": null, "age": null}' }],
usage: { input_tokens: 10, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

const result = await runScript(
['text', '--schema', '{"name":"string","age":"number"}'],
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);
const output = JSON.parse(result.stdout);
expect(output.extracted.name).toBeNull();
});

it('handles empty text input', async () => {
const result = await runScript(['--schema', '{"data":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(result.code).toBe(1);
expect(result.stderr).toContain('Usage');
});

it('handles special characters in schema', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"field_name": "value"}' }],
usage: { input_tokens: 10, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

const result = await runScript(
['text', '--schema', '{"field_name":"string"}'],
{ ANTHROPIC_API_KEY: 'test-key' }
);

expect(result.code).toBe(0);
});

it('validates fetch is called with correct anthropic headers', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
content: [{ text: '{"data": "test"}' }],
usage: { input_tokens: 10, output_tokens: 10 },
}),
});
global.fetch = mockFetch;

await runScript(['text', '--schema', '{"data":"string"}'], {
ANTHROPIC_API_KEY: 'test-key',
});

expect(mockFetch).toHaveBeenCalledWith(
'https://api.anthropic.com/v1/messages',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'x-api-key': 'test-key',
'anthropic-version': '2023-06-01',
}),
})
);
});
});
Loading
Loading