@goonnguyen/human-mcp 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -19
- package/bin/human-mcp.js +2 -0
- package/dist/index.js +65180 -1698
- package/package.json +19 -2
- package/.claude/agents/code-reviewer.md +0 -140
- package/.claude/agents/database-admin.md +0 -86
- package/.claude/agents/debugger.md +0 -119
- package/.claude/agents/docs-manager.md +0 -113
- package/.claude/agents/git-manager.md +0 -59
- package/.claude/agents/planner-researcher.md +0 -97
- package/.claude/agents/project-manager.md +0 -113
- package/.claude/agents/tester.md +0 -95
- package/.claude/commands/cook.md +0 -7
- package/.claude/commands/debug.md +0 -10
- package/.claude/commands/docs/init.md +0 -11
- package/.claude/commands/docs/update.md +0 -11
- package/.claude/commands/fix/ci.md +0 -8
- package/.claude/commands/fix/fast.md +0 -5
- package/.claude/commands/fix/hard.md +0 -7
- package/.claude/commands/fix/test.md +0 -16
- package/.claude/commands/git/cm.md +0 -5
- package/.claude/commands/git/cp.md +0 -4
- package/.claude/commands/plan/ci.md +0 -12
- package/.claude/commands/plan/two.md +0 -13
- package/.claude/commands/plan.md +0 -10
- package/.claude/commands/test.md +0 -7
- package/.claude/commands/watzup.md +0 -8
- package/.claude/hooks/telegram_notify.sh +0 -136
- package/.claude/send-discord.sh +0 -64
- package/.claude/settings.json +0 -7
- package/.claude/statusline.sh +0 -143
- package/.dockerignore +0 -81
- package/.env.example +0 -44
- package/.github/workflows/publish.yml +0 -88
- package/.opencode/agent/code-reviewer.md +0 -142
- package/.opencode/agent/debugger.md +0 -74
- package/.opencode/agent/docs-manager.md +0 -119
- package/.opencode/agent/git-manager.md +0 -60
- package/.opencode/agent/planner-researcher.md +0 -100
- package/.opencode/agent/project-manager.md +0 -113
- package/.opencode/agent/system-architecture.md +0 -200
- package/.opencode/agent/tester.md +0 -96
- package/.opencode/agent/ui-ux-developer.md +0 -97
- package/.opencode/command/cook.md +0 -7
- package/.opencode/command/debug.md +0 -10
- package/.opencode/command/fix/ci.md +0 -8
- package/.opencode/command/fix/fast.md +0 -5
- package/.opencode/command/fix/hard.md +0 -7
- package/.opencode/command/fix/test.md +0 -16
- package/.opencode/command/git/cm.md +0 -5
- package/.opencode/command/git/cp.md +0 -4
- package/.opencode/command/plan/ci.md +0 -12
- package/.opencode/command/plan/two.md +0 -13
- package/.opencode/command/plan.md +0 -10
- package/.opencode/command/test.md +0 -7
- package/.opencode/command/watzup.md +0 -8
- package/.releaserc.json +0 -26
- package/.serena/project.yml +0 -68
- package/CHANGELOG.md +0 -62
- package/CLAUDE.md +0 -141
- package/DEPLOYMENT.md +0 -329
- package/Dockerfile +0 -52
- package/QUICKSTART.md +0 -97
- package/bun.lock +0 -1872
- package/bunfig.toml +0 -15
- package/docker-compose.yaml +0 -128
- package/docs/README.md +0 -51
- package/docs/codebase-structure-architecture-code-standards.md +0 -428
- package/docs/codebase-summary.md +0 -321
- package/docs/project-overview-pdr.md +0 -286
- package/docs/project-roadmap.md +0 -494
- package/examples/debugging-session.ts +0 -96
- package/human-mcp.png +0 -0
- package/inspector-wrapper.mjs +0 -33
- package/plans/001-streamable-http-transport-plan.md +0 -905
- package/plans/002-sse-fallback-http-transport-plan.md +0 -161
- package/plans/003-fix-test-infrastructure-and-ci-plan.md +0 -699
- package/plans/003-http-transport-local-file-access-plan.md +0 -880
- package/plans/004-fix-typescript-compilation-errors-plan.md +0 -388
- package/plans/005-comprehensive-test-infrastructure-fix-plan.md +0 -854
- package/plans/templates/bug-fix-template.md +0 -69
- package/plans/templates/feature-implementation-template.md +0 -84
- package/plans/templates/refactor-template.md +0 -82
- package/plans/templates/template-usage-guide.md +0 -58
- package/src/index.ts +0 -49
- package/src/prompts/debugging-prompts.ts +0 -149
- package/src/prompts/index.ts +0 -55
- package/src/resources/documentation.ts +0 -316
- package/src/resources/index.ts +0 -49
- package/src/server.ts +0 -36
- package/src/tools/eyes/index.ts +0 -225
- package/src/tools/eyes/processors/gif.ts +0 -137
- package/src/tools/eyes/processors/image.ts +0 -213
- package/src/tools/eyes/processors/video.ts +0 -135
- package/src/tools/eyes/schemas.ts +0 -51
- package/src/tools/eyes/utils/formatters.ts +0 -126
- package/src/tools/eyes/utils/gemini-client.ts +0 -73
- package/src/transports/http/file-interceptor.ts +0 -134
- package/src/transports/http/middleware.ts +0 -46
- package/src/transports/http/routes.ts +0 -297
- package/src/transports/http/server.ts +0 -116
- package/src/transports/http/session.ts +0 -93
- package/src/transports/http/sse-routes.ts +0 -210
- package/src/transports/index.ts +0 -36
- package/src/transports/stdio.ts +0 -7
- package/src/transports/types.ts +0 -50
- package/src/types/index.ts +0 -41
- package/src/utils/cloudflare-r2.ts +0 -107
- package/src/utils/config.ts +0 -123
- package/src/utils/errors.ts +0 -40
- package/src/utils/logger.ts +0 -49
- package/tests/integration/http-transport-files.test.ts +0 -190
- package/tests/integration/server.test.ts +0 -27
- package/tests/integration/sse-transport.test.ts +0 -142
- package/tests/setup.ts +0 -55
- package/tests/types/api-responses.ts +0 -35
- package/tests/types/test-types.ts +0 -105
- package/tests/unit/cloudflare-r2.test.ts +0 -118
- package/tests/unit/config.test.ts +0 -40
- package/tests/unit/eyes-analyze.test.ts +0 -150
- package/tests/unit/formatters.test.ts +0 -85
- package/tests/unit/sse-routes.test.ts +0 -92
- package/tests/utils/error-scenarios.ts +0 -198
- package/tests/utils/index.ts +0 -3
- package/tests/utils/mock-helpers.ts +0 -99
- package/tests/utils/test-data-generators.ts +0 -217
- package/tests/utils/test-server-manager.ts +0 -172
- package/tsconfig.json +0 -26
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, mock } from 'bun:test';
|
|
2
|
-
import { CloudflareR2Client, getCloudflareR2 } from '@/utils/cloudflare-r2';
|
|
3
|
-
import type { MockS3Command, MockCloudflareR2Client } from '../types/test-types.js';
|
|
4
|
-
|
|
5
|
-
// Mock the S3Client and PutObjectCommand
|
|
6
|
-
mock.module('@aws-sdk/client-s3', () => ({
|
|
7
|
-
S3Client: mock(() => ({})),
|
|
8
|
-
PutObjectCommand: mock((params: MockS3Command) => ({ ...params }))
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
describe('Cloudflare R2 Integration', () => {
|
|
12
|
-
beforeAll(() => {
|
|
13
|
-
// Set up test environment variables
|
|
14
|
-
process.env.CLOUDFLARE_CDN_ACCESS_KEY = 'test-access-key';
|
|
15
|
-
process.env.CLOUDFLARE_CDN_SECRET_KEY = 'test-secret-key';
|
|
16
|
-
process.env.CLOUDFLARE_CDN_ENDPOINT_URL = 'https://test-account.r2.cloudflarestorage.com';
|
|
17
|
-
process.env.CLOUDFLARE_CDN_BUCKET_NAME = 'test-bucket';
|
|
18
|
-
process.env.CLOUDFLARE_CDN_BASE_URL = 'https://cdn.test.com';
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should create CloudflareR2Client with correct configuration', () => {
|
|
22
|
-
expect(() => new CloudflareR2Client()).not.toThrow();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should throw error when required environment variables are missing', () => {
|
|
26
|
-
const originalAccessKey = process.env.CLOUDFLARE_CDN_ACCESS_KEY;
|
|
27
|
-
delete process.env.CLOUDFLARE_CDN_ACCESS_KEY;
|
|
28
|
-
|
|
29
|
-
expect(() => new CloudflareR2Client()).toThrow('Missing required Cloudflare R2 environment variables');
|
|
30
|
-
|
|
31
|
-
process.env.CLOUDFLARE_CDN_ACCESS_KEY = originalAccessKey;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should check if Cloudflare R2 is configured', () => {
|
|
35
|
-
const client = new CloudflareR2Client();
|
|
36
|
-
expect(client.isConfigured()).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should return null when getCloudflareR2() called without configuration', () => {
|
|
40
|
-
// Temporarily remove configuration
|
|
41
|
-
const originalEnvs = {
|
|
42
|
-
CLOUDFLARE_CDN_ACCESS_KEY: process.env.CLOUDFLARE_CDN_ACCESS_KEY,
|
|
43
|
-
CLOUDFLARE_CDN_SECRET_KEY: process.env.CLOUDFLARE_CDN_SECRET_KEY,
|
|
44
|
-
CLOUDFLARE_CDN_ENDPOINT_URL: process.env.CLOUDFLARE_CDN_ENDPOINT_URL,
|
|
45
|
-
CLOUDFLARE_CDN_BUCKET_NAME: process.env.CLOUDFLARE_CDN_BUCKET_NAME,
|
|
46
|
-
CLOUDFLARE_CDN_BASE_URL: process.env.CLOUDFLARE_CDN_BASE_URL,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
Object.keys(originalEnvs).forEach(key => delete process.env[key]);
|
|
50
|
-
|
|
51
|
-
const client = getCloudflareR2();
|
|
52
|
-
expect(client).toBeNull();
|
|
53
|
-
|
|
54
|
-
// Restore environment variables
|
|
55
|
-
Object.assign(process.env, originalEnvs);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should generate proper file keys with UUID', async () => {
|
|
59
|
-
const client = new CloudflareR2Client();
|
|
60
|
-
const testBuffer = Buffer.from('test file content');
|
|
61
|
-
|
|
62
|
-
// Mock the S3 send method to capture the command
|
|
63
|
-
let capturedCommand: MockS3Command | undefined;
|
|
64
|
-
const mockSend = mock(async (command: MockS3Command) => {
|
|
65
|
-
capturedCommand = command;
|
|
66
|
-
return {};
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
(client as unknown as MockCloudflareR2Client).s3Client.send = mockSend;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await client.uploadFile(testBuffer, 'test.jpg');
|
|
73
|
-
|
|
74
|
-
expect(capturedCommand).toBeDefined();
|
|
75
|
-
expect(capturedCommand!.input.Key).toMatch(/^human-mcp\/[a-f0-9-]{36}\.jpg$/);
|
|
76
|
-
expect(capturedCommand!.input.ContentType).toBe('image/jpeg');
|
|
77
|
-
expect(capturedCommand!.input.Metadata?.originalName).toBe('test.jpg');
|
|
78
|
-
} catch (error) {
|
|
79
|
-
// Expected to fail in test environment, but we captured the command
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should handle base64 upload correctly', async () => {
|
|
84
|
-
const client = new CloudflareR2Client();
|
|
85
|
-
const testBase64 = Buffer.from('test image data').toString('base64');
|
|
86
|
-
|
|
87
|
-
let capturedCommand: MockS3Command | undefined;
|
|
88
|
-
const mockSend = mock(async (command: MockS3Command) => {
|
|
89
|
-
capturedCommand = command;
|
|
90
|
-
return {};
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
(client as unknown as MockCloudflareR2Client).s3Client.send = mockSend;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
await client.uploadBase64(testBase64, 'image/png', 'test.png');
|
|
97
|
-
|
|
98
|
-
expect(capturedCommand).toBeDefined();
|
|
99
|
-
expect(capturedCommand!.input.Key).toMatch(/^human-mcp\/[a-f0-9-]{36}\.png$/);
|
|
100
|
-
expect(capturedCommand!.input.ContentType).toBe('image/png');
|
|
101
|
-
} catch (error) {
|
|
102
|
-
// Expected to fail in test environment
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should handle upload errors gracefully', async () => {
|
|
107
|
-
const client = new CloudflareR2Client();
|
|
108
|
-
|
|
109
|
-
const mockSend = mock(async () => {
|
|
110
|
-
throw new Error('Network error');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
(client as unknown as MockCloudflareR2Client).s3Client.send = mockSend;
|
|
114
|
-
|
|
115
|
-
await expect(client.uploadFile(Buffer.from('test'), 'test.jpg'))
|
|
116
|
-
.rejects.toThrow('Failed to upload file: Network error');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
-
import { loadConfig } from "../../src/utils/config.js";
|
|
3
|
-
|
|
4
|
-
describe("Config", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
process.env.GOOGLE_GEMINI_API_KEY = "test-key";
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("should load default configuration", () => {
|
|
10
|
-
process.env.LOG_LEVEL = "info"; // Override test setup
|
|
11
|
-
const config = loadConfig();
|
|
12
|
-
|
|
13
|
-
expect(config.gemini.apiKey).toBe("test-key");
|
|
14
|
-
expect(config.gemini.model).toBe("gemini-2.5-flash");
|
|
15
|
-
expect(config.server.port).toBe(3000);
|
|
16
|
-
expect(config.logging.level).toBe("info");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("should override defaults with environment variables", () => {
|
|
20
|
-
process.env.PORT = "8080";
|
|
21
|
-
process.env.LOG_LEVEL = "debug";
|
|
22
|
-
process.env.GOOGLE_GEMINI_MODEL = "gemini-2.5-flash";
|
|
23
|
-
|
|
24
|
-
const config = loadConfig();
|
|
25
|
-
|
|
26
|
-
expect(config.server.port).toBe(8080);
|
|
27
|
-
expect(config.logging.level).toBe("debug");
|
|
28
|
-
expect(config.gemini.model).toBe("gemini-2.5-flash");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("should throw error for missing API key", () => {
|
|
32
|
-
// Clear environment variables set by other tests
|
|
33
|
-
delete process.env.GOOGLE_GEMINI_API_KEY;
|
|
34
|
-
delete process.env.PORT;
|
|
35
|
-
delete process.env.LOG_LEVEL;
|
|
36
|
-
delete process.env.GOOGLE_GEMINI_MODEL;
|
|
37
|
-
|
|
38
|
-
expect(() => loadConfig()).toThrow();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, mock } from 'bun:test';
|
|
2
|
-
import { registerEyesTool } from '@/tools/eyes/index';
|
|
3
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
-
import { loadConfig } from '@/utils/config';
|
|
5
|
-
import { MockHelpers, TestDataGenerators } from '../utils/index.js';
|
|
6
|
-
|
|
7
|
-
// Import global mocks from setup
|
|
8
|
-
import { globalMocks } from '../setup.js';
|
|
9
|
-
|
|
10
|
-
// Mock fetch for URL operations
|
|
11
|
-
const mockFetch = mock(async (url: string) => {
|
|
12
|
-
if (url.includes('error')) {
|
|
13
|
-
throw new Error('Fetch failed');
|
|
14
|
-
}
|
|
15
|
-
return new Response(TestDataGenerators.createMockImageBuffer(), {
|
|
16
|
-
status: 200,
|
|
17
|
-
headers: { 'content-type': 'image/jpeg' }
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
global.fetch = mockFetch as unknown as typeof fetch;
|
|
21
|
-
|
|
22
|
-
// Mock Gemini client
|
|
23
|
-
const mockGeminiModel = {
|
|
24
|
-
generateContent: mock(async () => ({
|
|
25
|
-
response: {
|
|
26
|
-
text: () => JSON.stringify(TestDataGenerators.createMockGeminiResponse())
|
|
27
|
-
}
|
|
28
|
-
}))
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const mockGeminiClient = {
|
|
32
|
-
getModel: mock(() => mockGeminiModel)
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
mock.module('@/tools/eyes/utils/gemini-client', () => ({
|
|
36
|
-
GeminiClient: mock(() => mockGeminiClient)
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
// Mock processors
|
|
40
|
-
mock.module('@/tools/eyes/processors/image', () => ({
|
|
41
|
-
processImage: mock(async () => ({
|
|
42
|
-
analysis: JSON.stringify(TestDataGenerators.createMockGeminiResponse())
|
|
43
|
-
}))
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
mock.module('@/tools/eyes/processors/video', () => ({
|
|
47
|
-
processVideo: mock(async () => ({
|
|
48
|
-
analysis: JSON.stringify(TestDataGenerators.createMockGeminiResponse())
|
|
49
|
-
}))
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
mock.module('@/tools/eyes/processors/gif', () => ({
|
|
53
|
-
processGif: mock(async () => ({
|
|
54
|
-
analysis: JSON.stringify(TestDataGenerators.createMockGeminiResponse())
|
|
55
|
-
}))
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
describe('Eyes Analyze Tool', () => {
|
|
59
|
-
let server: McpServer;
|
|
60
|
-
|
|
61
|
-
beforeAll(async () => {
|
|
62
|
-
process.env.GOOGLE_GEMINI_API_KEY = 'test-key';
|
|
63
|
-
|
|
64
|
-
const config = loadConfig();
|
|
65
|
-
|
|
66
|
-
server = new McpServer({
|
|
67
|
-
name: 'test-server',
|
|
68
|
-
version: '1.0.0'
|
|
69
|
-
}, {
|
|
70
|
-
capabilities: {
|
|
71
|
-
tools: {}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await registerEyesTool(server, config);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
afterAll(() => {
|
|
79
|
-
delete process.env.GOOGLE_GEMINI_API_KEY;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
// Reset mocks before each test
|
|
84
|
-
MockHelpers.resetAllMocks({
|
|
85
|
-
mockGeminiModel,
|
|
86
|
-
mockGeminiClient,
|
|
87
|
-
mockFetch
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('tool registration', () => {
|
|
92
|
-
it('should register eyes_analyze tool successfully', () => {
|
|
93
|
-
// Test that the registration process completed successfully
|
|
94
|
-
expect(server).toBeDefined();
|
|
95
|
-
expect(server).toBeInstanceOf(McpServer);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should register eyes_compare tool successfully', () => {
|
|
99
|
-
// Test that the registration process completed successfully
|
|
100
|
-
expect(server).toBeDefined();
|
|
101
|
-
expect(server).toBeInstanceOf(McpServer);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should register tools without errors', () => {
|
|
105
|
-
expect(server).toBeInstanceOf(McpServer);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('eyes_analyze schema validation', () => {
|
|
110
|
-
it('should validate schema registration without errors', () => {
|
|
111
|
-
// Test that schema registration completes successfully
|
|
112
|
-
expect(server).toBeDefined();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should handle mock processor calls', async () => {
|
|
116
|
-
// Test that the mocked processors can be called
|
|
117
|
-
const { processImage } = await import('@/tools/eyes/processors/image');
|
|
118
|
-
const result = await (processImage as unknown as () => Promise<{ analysis: string }>)();
|
|
119
|
-
expect(result.analysis).toContain('summary');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should handle mock Gemini client calls', () => {
|
|
123
|
-
// Test that the mocked Gemini client can be instantiated and called
|
|
124
|
-
expect(mockGeminiClient.getModel).toBeDefined();
|
|
125
|
-
expect(mockGeminiModel.generateContent).toBeDefined();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('eyes_compare schema validation', () => {
|
|
130
|
-
it('should validate comparison schema registration', () => {
|
|
131
|
-
// Test that comparison tool registration completes successfully
|
|
132
|
-
expect(server).toBeDefined();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should handle mock data generation', () => {
|
|
136
|
-
// Test that mock data generators work correctly
|
|
137
|
-
const compareRequest = TestDataGenerators.createMockCompareRequest();
|
|
138
|
-
expect(compareRequest.input1).toContain('data:image/png;base64');
|
|
139
|
-
expect(compareRequest.input2).toContain('data:image/png;base64');
|
|
140
|
-
expect(['pixel', 'structural', 'semantic']).toContain(compareRequest.comparison_type);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('error handling', () => {
|
|
145
|
-
it('should handle registration errors gracefully', () => {
|
|
146
|
-
// Test that error handling is set up correctly
|
|
147
|
-
expect(server).toBeInstanceOf(McpServer);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { createPrompt, parseAnalysisResponse } from "../../src/tools/eyes/utils/formatters.js";
|
|
3
|
-
import type { AnalysisOptions } from "../../src/types/index.js";
|
|
4
|
-
|
|
5
|
-
describe("Formatters", () => {
|
|
6
|
-
describe("createPrompt", () => {
|
|
7
|
-
it("should create UI debug prompt", () => {
|
|
8
|
-
const options: AnalysisOptions = {
|
|
9
|
-
analysis_type: "ui_debug",
|
|
10
|
-
detail_level: "detailed"
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const prompt = createPrompt(options);
|
|
14
|
-
|
|
15
|
-
expect(prompt).toContain("UI debugging expert");
|
|
16
|
-
expect(prompt).toContain("layout issues");
|
|
17
|
-
expect(prompt).toContain("thorough analysis");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("should create accessibility prompt", () => {
|
|
21
|
-
const options: AnalysisOptions = {
|
|
22
|
-
analysis_type: "accessibility",
|
|
23
|
-
detail_level: "quick"
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const prompt = createPrompt(options);
|
|
27
|
-
|
|
28
|
-
expect(prompt).toContain("accessibility expert");
|
|
29
|
-
expect(prompt).toContain("WCAG guidelines");
|
|
30
|
-
expect(prompt).toContain("Provide a concise analysis");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should include specific focus when provided", () => {
|
|
34
|
-
const options: AnalysisOptions = {
|
|
35
|
-
analysis_type: "general",
|
|
36
|
-
detail_level: "detailed",
|
|
37
|
-
specific_focus: "login form errors"
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const prompt = createPrompt(options);
|
|
41
|
-
|
|
42
|
-
expect(prompt).toContain("login form errors");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("parseAnalysisResponse", () => {
|
|
47
|
-
it("should parse structured response", () => {
|
|
48
|
-
const response = `
|
|
49
|
-
OVERVIEW: This is a test analysis
|
|
50
|
-
|
|
51
|
-
KEY FINDINGS: Found several issues
|
|
52
|
-
|
|
53
|
-
DETAILED ANALYSIS: Detailed breakdown of issues
|
|
54
|
-
|
|
55
|
-
UI ELEMENTS: Button at 100,200 size 150x50
|
|
56
|
-
|
|
57
|
-
RECOMMENDATIONS: Fix the layout issues
|
|
58
|
-
|
|
59
|
-
DEBUGGING INSIGHTS: Consider responsive design
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
const parsed = parseAnalysisResponse(response);
|
|
63
|
-
|
|
64
|
-
expect(parsed.description).toContain("This is a test analysis");
|
|
65
|
-
expect(parsed.analysis).toContain("Detailed breakdown of issues");
|
|
66
|
-
expect(parsed.recommendations).toContain("Fix the layout issues");
|
|
67
|
-
expect(parsed.insights).toContain("Consider responsive design");
|
|
68
|
-
expect(parsed.elements).toHaveLength(1);
|
|
69
|
-
expect(parsed.elements?.[0]?.location).toEqual({
|
|
70
|
-
x: 100, y: 200, width: 150, height: 50
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("should handle missing sections gracefully", () => {
|
|
75
|
-
const response = "Simple analysis without sections";
|
|
76
|
-
|
|
77
|
-
const parsed = parseAnalysisResponse(response);
|
|
78
|
-
|
|
79
|
-
expect(parsed.description).toBe("Simple analysis without sections");
|
|
80
|
-
expect(parsed.analysis).toBe("Simple analysis without sections");
|
|
81
|
-
expect(parsed.elements).toEqual([]);
|
|
82
|
-
expect(parsed.insights).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { SSEManager } from "../../src/transports/http/sse-routes.js";
|
|
3
|
-
import type { HttpTransportConfig } from "../../src/transports/types.js";
|
|
4
|
-
import type { Response } from "express";
|
|
5
|
-
|
|
6
|
-
describe("SSEManager", () => {
|
|
7
|
-
let sseManager: SSEManager;
|
|
8
|
-
let config: HttpTransportConfig;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
config = {
|
|
12
|
-
port: 3000,
|
|
13
|
-
sessionMode: "stateful",
|
|
14
|
-
enableSseFallback: true,
|
|
15
|
-
ssePaths: {
|
|
16
|
-
stream: "/sse",
|
|
17
|
-
message: "/messages"
|
|
18
|
-
},
|
|
19
|
-
security: {
|
|
20
|
-
enableDnsRebindingProtection: true,
|
|
21
|
-
allowedHosts: ["127.0.0.1", "localhost"]
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
sseManager = new SSEManager(config);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
await sseManager.cleanup();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("session management", () => {
|
|
32
|
-
it("should start with zero sessions", () => {
|
|
33
|
-
expect(sseManager.getSessionCount()).toBe(0);
|
|
34
|
-
expect(sseManager.hasSession("non-existent")).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("should track session existence correctly", () => {
|
|
38
|
-
// Mock response object for testing
|
|
39
|
-
const mockRes = {
|
|
40
|
-
setHeader: () => {},
|
|
41
|
-
write: () => {},
|
|
42
|
-
end: () => {},
|
|
43
|
-
on: () => {},
|
|
44
|
-
removeAllListeners: () => {}
|
|
45
|
-
} as unknown as Response;
|
|
46
|
-
|
|
47
|
-
const transport = sseManager.createSession("/messages", mockRes);
|
|
48
|
-
|
|
49
|
-
expect(sseManager.getSessionCount()).toBe(1);
|
|
50
|
-
expect(sseManager.hasSession(transport.sessionId)).toBe(true);
|
|
51
|
-
expect(sseManager.getSession(transport.sessionId)).toBe(transport);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("should return null for non-existent session", () => {
|
|
55
|
-
expect(sseManager.getSession("non-existent-id")).toBe(null);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("should cleanup sessions correctly", async () => {
|
|
59
|
-
const mockRes = {
|
|
60
|
-
setHeader: () => {},
|
|
61
|
-
write: () => {},
|
|
62
|
-
end: () => {},
|
|
63
|
-
on: () => {},
|
|
64
|
-
removeAllListeners: () => {}
|
|
65
|
-
} as unknown as Response;
|
|
66
|
-
|
|
67
|
-
sseManager.createSession("/messages", mockRes);
|
|
68
|
-
expect(sseManager.getSessionCount()).toBe(1);
|
|
69
|
-
|
|
70
|
-
await sseManager.cleanup();
|
|
71
|
-
expect(sseManager.getSessionCount()).toBe(0);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("configuration handling", () => {
|
|
76
|
-
it("should use security configuration from config", () => {
|
|
77
|
-
const mockRes = {
|
|
78
|
-
setHeader: () => {},
|
|
79
|
-
write: () => {},
|
|
80
|
-
end: () => {},
|
|
81
|
-
on: () => {},
|
|
82
|
-
removeAllListeners: () => {}
|
|
83
|
-
} as unknown as Response;
|
|
84
|
-
|
|
85
|
-
const transport = sseManager.createSession("/messages", mockRes);
|
|
86
|
-
|
|
87
|
-
// Transport should be created successfully with security config
|
|
88
|
-
expect(transport).toBeDefined();
|
|
89
|
-
expect(transport.sessionId).toBeDefined();
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { mock } from 'bun:test';
|
|
2
|
-
import type { MockError } from '../types/test-types.js';
|
|
3
|
-
|
|
4
|
-
export class ErrorScenarios {
|
|
5
|
-
/**
|
|
6
|
-
* Common network errors for testing
|
|
7
|
-
*/
|
|
8
|
-
static networkErrors = {
|
|
9
|
-
CONNECTION_REFUSED: new Error('ECONNREFUSED: Connection refused'),
|
|
10
|
-
TIMEOUT: new Error('ETIMEDOUT: Request timeout'),
|
|
11
|
-
DNS_ERROR: new Error('ENOTFOUND: DNS lookup failed'),
|
|
12
|
-
SSL_ERROR: new Error('SSL certificate verification failed'),
|
|
13
|
-
NETWORK_UNREACHABLE: new Error('ENETUNREACH: Network is unreachable')
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* HTTP error responses
|
|
18
|
-
*/
|
|
19
|
-
static httpErrors = {
|
|
20
|
-
NOT_FOUND: { status: 404, error: 'Resource not found' },
|
|
21
|
-
UNAUTHORIZED: { status: 401, error: 'Unauthorized access' },
|
|
22
|
-
FORBIDDEN: { status: 403, error: 'Forbidden' },
|
|
23
|
-
SERVER_ERROR: { status: 500, error: 'Internal server error' },
|
|
24
|
-
BAD_GATEWAY: { status: 502, error: 'Bad gateway' },
|
|
25
|
-
SERVICE_UNAVAILABLE: { status: 503, error: 'Service unavailable' },
|
|
26
|
-
RATE_LIMITED: { status: 429, error: 'Too many requests' }
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* API specific errors
|
|
31
|
-
*/
|
|
32
|
-
static apiErrors = {
|
|
33
|
-
GEMINI_API_ERROR: new Error('Gemini API quota exceeded'),
|
|
34
|
-
GEMINI_INVALID_KEY: new Error('Invalid Gemini API key'),
|
|
35
|
-
GEMINI_MODEL_UNAVAILABLE: new Error('Gemini model temporarily unavailable'),
|
|
36
|
-
CLOUDFLARE_R2_ERROR: new Error('Cloudflare R2 upload failed'),
|
|
37
|
-
FILE_NOT_FOUND: new Error('ENOENT: File not found'),
|
|
38
|
-
PERMISSION_DENIED: new Error('EACCES: Permission denied'),
|
|
39
|
-
DISK_FULL: new Error('ENOSPC: No space left on device')
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create a mock that fails with a specific error
|
|
44
|
-
*/
|
|
45
|
-
static createFailingMock<T>(error: Error | MockError): ReturnType<typeof mock> {
|
|
46
|
-
return mock(async (..._args: any[]): Promise<T> => {
|
|
47
|
-
throw error;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Create a mock that fails intermittently
|
|
53
|
-
*/
|
|
54
|
-
static createIntermittentMock<T>(
|
|
55
|
-
successValue: T,
|
|
56
|
-
error: Error | MockError,
|
|
57
|
-
failureRate = 0.5
|
|
58
|
-
): ReturnType<typeof mock> {
|
|
59
|
-
return mock(async (..._args: any[]): Promise<T> => {
|
|
60
|
-
if (Math.random() < failureRate) {
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
return successValue;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Create a mock that times out
|
|
69
|
-
*/
|
|
70
|
-
static createTimeoutMock<T>(delay = 5000): ReturnType<typeof mock> {
|
|
71
|
-
return mock(async (..._args: any[]): Promise<T> => {
|
|
72
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
73
|
-
throw new Error('Request timeout');
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Create a mock fetch that returns HTTP errors
|
|
79
|
-
*/
|
|
80
|
-
static createErrorResponse(errorType: keyof typeof ErrorScenarios.httpErrors): Response {
|
|
81
|
-
const error = ErrorScenarios.httpErrors[errorType];
|
|
82
|
-
return new Response(JSON.stringify({ error: error.error }), {
|
|
83
|
-
status: error.status,
|
|
84
|
-
statusText: error.error,
|
|
85
|
-
headers: { 'Content-Type': 'application/json' }
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Create test scenarios for network resilience
|
|
91
|
-
*/
|
|
92
|
-
static createNetworkResilienceTests() {
|
|
93
|
-
return {
|
|
94
|
-
'should handle connection refused': {
|
|
95
|
-
error: ErrorScenarios.networkErrors.CONNECTION_REFUSED,
|
|
96
|
-
expectedMessage: 'Connection refused'
|
|
97
|
-
},
|
|
98
|
-
'should handle timeout': {
|
|
99
|
-
error: ErrorScenarios.networkErrors.TIMEOUT,
|
|
100
|
-
expectedMessage: 'Request timeout'
|
|
101
|
-
},
|
|
102
|
-
'should handle DNS errors': {
|
|
103
|
-
error: ErrorScenarios.networkErrors.DNS_ERROR,
|
|
104
|
-
expectedMessage: 'DNS lookup failed'
|
|
105
|
-
},
|
|
106
|
-
'should handle SSL errors': {
|
|
107
|
-
error: ErrorScenarios.networkErrors.SSL_ERROR,
|
|
108
|
-
expectedMessage: 'SSL certificate verification failed'
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Create test scenarios for API errors
|
|
115
|
-
*/
|
|
116
|
-
static createAPIErrorTests() {
|
|
117
|
-
return {
|
|
118
|
-
'should handle Gemini API quota exceeded': {
|
|
119
|
-
error: ErrorScenarios.apiErrors.GEMINI_API_ERROR,
|
|
120
|
-
expectedMessage: 'Gemini API quota exceeded'
|
|
121
|
-
},
|
|
122
|
-
'should handle invalid API key': {
|
|
123
|
-
error: ErrorScenarios.apiErrors.GEMINI_INVALID_KEY,
|
|
124
|
-
expectedMessage: 'Invalid Gemini API key'
|
|
125
|
-
},
|
|
126
|
-
'should handle model unavailable': {
|
|
127
|
-
error: ErrorScenarios.apiErrors.GEMINI_MODEL_UNAVAILABLE,
|
|
128
|
-
expectedMessage: 'model temporarily unavailable'
|
|
129
|
-
},
|
|
130
|
-
'should handle file upload errors': {
|
|
131
|
-
error: ErrorScenarios.apiErrors.CLOUDFLARE_R2_ERROR,
|
|
132
|
-
expectedMessage: 'upload failed'
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Create test scenarios for file system errors
|
|
139
|
-
*/
|
|
140
|
-
static createFileSystemErrorTests() {
|
|
141
|
-
return {
|
|
142
|
-
'should handle file not found': {
|
|
143
|
-
error: ErrorScenarios.apiErrors.FILE_NOT_FOUND,
|
|
144
|
-
expectedMessage: 'File not found'
|
|
145
|
-
},
|
|
146
|
-
'should handle permission denied': {
|
|
147
|
-
error: ErrorScenarios.apiErrors.PERMISSION_DENIED,
|
|
148
|
-
expectedMessage: 'Permission denied'
|
|
149
|
-
},
|
|
150
|
-
'should handle disk full': {
|
|
151
|
-
error: ErrorScenarios.apiErrors.DISK_FULL,
|
|
152
|
-
expectedMessage: 'No space left on device'
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Simulate retry logic testing
|
|
159
|
-
*/
|
|
160
|
-
static createRetryMock<T>(
|
|
161
|
-
finalResult: T,
|
|
162
|
-
failures: (Error | MockError)[],
|
|
163
|
-
maxRetries = 3
|
|
164
|
-
): ReturnType<typeof mock> {
|
|
165
|
-
let attemptCount = 0;
|
|
166
|
-
|
|
167
|
-
return mock(async (..._args: any[]): Promise<T> => {
|
|
168
|
-
if (attemptCount < failures.length && attemptCount < maxRetries) {
|
|
169
|
-
attemptCount++;
|
|
170
|
-
throw failures[attemptCount - 1];
|
|
171
|
-
}
|
|
172
|
-
attemptCount++;
|
|
173
|
-
return finalResult;
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Test concurrent failure scenarios
|
|
179
|
-
*/
|
|
180
|
-
static createConcurrentFailureMock<T>(
|
|
181
|
-
results: (T | Error)[]
|
|
182
|
-
): ReturnType<typeof mock> {
|
|
183
|
-
let callIndex = 0;
|
|
184
|
-
|
|
185
|
-
return mock(async (..._args: any[]): Promise<T> => {
|
|
186
|
-
const result = results[callIndex % results.length];
|
|
187
|
-
callIndex++;
|
|
188
|
-
|
|
189
|
-
if (result instanceof Error) {
|
|
190
|
-
throw result;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return result as T;
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export default ErrorScenarios;
|
package/tests/utils/index.ts
DELETED