@goonnguyen/human-mcp 1.2.0 → 1.3.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.
Files changed (71) hide show
  1. package/.claude/agents/project-manager.md +2 -2
  2. package/.env.example +28 -1
  3. package/.github/workflows/publish.yml +43 -6
  4. package/.opencode/agent/code-reviewer.md +142 -0
  5. package/.opencode/agent/debugger.md +74 -0
  6. package/.opencode/agent/docs-manager.md +119 -0
  7. package/.opencode/agent/git-manager.md +60 -0
  8. package/.opencode/agent/planner-researcher.md +100 -0
  9. package/.opencode/agent/project-manager.md +113 -0
  10. package/.opencode/agent/system-architecture.md +200 -0
  11. package/.opencode/agent/tester.md +96 -0
  12. package/.opencode/agent/ui-ux-developer.md +97 -0
  13. package/.opencode/command/cook.md +7 -0
  14. package/.opencode/command/debug.md +10 -0
  15. package/.opencode/command/fix/ci.md +8 -0
  16. package/.opencode/command/fix/fast.md +5 -0
  17. package/.opencode/command/fix/hard.md +7 -0
  18. package/.opencode/command/fix/test.md +16 -0
  19. package/.opencode/command/git/cm.md +5 -0
  20. package/.opencode/command/git/cp.md +4 -0
  21. package/.opencode/command/plan/ci.md +12 -0
  22. package/.opencode/command/plan/two.md +13 -0
  23. package/.opencode/command/plan.md +10 -0
  24. package/.opencode/command/test.md +7 -0
  25. package/.opencode/command/watzup.md +8 -0
  26. package/CHANGELOG.md +21 -0
  27. package/CLAUDE.md +5 -3
  28. package/QUICKSTART.md +3 -3
  29. package/README.md +551 -20
  30. package/bun.lock +275 -3
  31. package/dist/index.js +71091 -17256
  32. package/docs/README.md +51 -0
  33. package/docs/codebase-structure-architecture-code-standards.md +17 -5
  34. package/docs/project-overview-pdr.md +37 -21
  35. package/docs/project-roadmap.md +494 -0
  36. package/human-mcp.png +0 -0
  37. package/package.json +9 -1
  38. package/plans/002-sse-fallback-http-transport-plan.md +161 -0
  39. package/plans/003-fix-test-infrastructure-and-ci-plan.md +699 -0
  40. package/plans/003-http-transport-local-file-access-plan.md +880 -0
  41. package/plans/004-fix-typescript-compilation-errors-plan.md +388 -0
  42. package/plans/005-comprehensive-test-infrastructure-fix-plan.md +854 -0
  43. package/src/index.ts +2 -0
  44. package/src/tools/eyes/index.ts +7 -7
  45. package/src/tools/eyes/processors/image.ts +90 -0
  46. package/src/transports/http/file-interceptor.ts +134 -0
  47. package/src/transports/http/routes.ts +165 -4
  48. package/src/transports/http/server.ts +64 -14
  49. package/src/transports/http/session.ts +11 -3
  50. package/src/transports/http/sse-routes.ts +210 -0
  51. package/src/transports/index.ts +11 -6
  52. package/src/transports/types.ts +13 -0
  53. package/src/utils/cloudflare-r2.ts +107 -0
  54. package/src/utils/config.ts +26 -0
  55. package/tests/integration/http-transport-files.test.ts +190 -0
  56. package/tests/integration/server.test.ts +4 -1
  57. package/tests/integration/sse-transport.test.ts +142 -0
  58. package/tests/setup.ts +45 -1
  59. package/tests/types/api-responses.ts +35 -0
  60. package/tests/types/test-types.ts +105 -0
  61. package/tests/unit/cloudflare-r2.test.ts +118 -0
  62. package/tests/unit/eyes-analyze.test.ts +150 -0
  63. package/tests/unit/formatters.test.ts +1 -1
  64. package/tests/unit/sse-routes.test.ts +92 -0
  65. package/tests/utils/error-scenarios.ts +198 -0
  66. package/tests/utils/index.ts +3 -0
  67. package/tests/utils/mock-helpers.ts +99 -0
  68. package/tests/utils/test-data-generators.ts +217 -0
  69. package/tests/utils/test-server-manager.ts +172 -0
  70. package/tsconfig.json +1 -1
  71. package/plans/reports/001-from-qa-engineer-to-development-team-test-suite-report.md +0 -188
@@ -0,0 +1,92 @@
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
+ });
@@ -0,0 +1,198 @@
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;
@@ -0,0 +1,3 @@
1
+ export { TestServerManager, testServerManager } from './test-server-manager.js';
2
+ export { MockHelpers } from './mock-helpers.js';
3
+ export { TestDataGenerators } from './test-data-generators.js';
@@ -0,0 +1,99 @@
1
+ import { mock, type Mock } from 'bun:test';
2
+ import type { MockError, MockHttpResponseData } from '../types/test-types.js';
3
+
4
+ export interface MockedLogger {
5
+ info: Mock<() => void>;
6
+ error: Mock<() => void>;
7
+ warn: Mock<() => void>;
8
+ debug: Mock<() => void>;
9
+ }
10
+
11
+ export interface MockedFS {
12
+ readFileSync: Mock<() => Buffer>;
13
+ writeFileSync: Mock<() => void>;
14
+ existsSync: Mock<() => boolean>;
15
+ mkdirSync: Mock<() => void>;
16
+ unlinkSync: Mock<() => void>;
17
+ }
18
+
19
+ export interface MockedGeminiClient {
20
+ generateContent: Mock<() => Promise<any>>;
21
+ getGenerativeModel: Mock<() => any>;
22
+ }
23
+
24
+ export class MockHelpers {
25
+ static createLoggerMock(): MockedLogger {
26
+ return {
27
+ info: mock(() => {}),
28
+ error: mock(() => {}),
29
+ warn: mock(() => {}),
30
+ debug: mock(() => {})
31
+ };
32
+ }
33
+
34
+ static createFileSystemMock(): MockedFS {
35
+ return {
36
+ readFileSync: mock(() => Buffer.from('mock file content')),
37
+ writeFileSync: mock(() => {}),
38
+ existsSync: mock(() => true),
39
+ mkdirSync: mock(() => {}),
40
+ unlinkSync: mock(() => {})
41
+ };
42
+ }
43
+
44
+ static createGeminiClientMock(): MockedGeminiClient {
45
+ return {
46
+ generateContent: mock(() => Promise.resolve({
47
+ response: {
48
+ text: () => JSON.stringify({
49
+ summary: "Mock analysis result",
50
+ details: "Mock detailed analysis",
51
+ confidence: 0.95
52
+ })
53
+ }
54
+ })),
55
+ getGenerativeModel: mock(() => ({
56
+ generateContent: mock(() => Promise.resolve({
57
+ response: {
58
+ text: () => JSON.stringify({
59
+ summary: "Mock analysis result",
60
+ details: "Mock detailed analysis",
61
+ confidence: 0.95
62
+ })
63
+ }
64
+ }))
65
+ }))
66
+ };
67
+ }
68
+
69
+ static resetAllMocks(mocks: Record<string, unknown>): void {
70
+ Object.values(mocks).forEach(mockObj => {
71
+ if (typeof mockObj === 'object' && mockObj !== null) {
72
+ Object.values(mockObj).forEach(mockFn => {
73
+ if (typeof mockFn === 'function' && 'mockRestore' in mockFn) {
74
+ (mockFn as Mock<any>).mockRestore();
75
+ }
76
+ });
77
+ }
78
+ });
79
+ }
80
+
81
+ static createMockResponse(data: MockHttpResponseData, status = 200): Response {
82
+ return new Response(JSON.stringify(data), {
83
+ status,
84
+ headers: {
85
+ 'Content-Type': 'application/json'
86
+ }
87
+ });
88
+ }
89
+
90
+ static createMockError(message: string, code?: string | number): MockError {
91
+ const error: MockError = { message };
92
+ if (code) {
93
+ error.code = code;
94
+ }
95
+ return error as MockError & Error;
96
+ }
97
+ }
98
+
99
+ export default MockHelpers;
@@ -0,0 +1,217 @@
1
+ import type { MockAnalysisRequest, MockCompareRequest, MockGeminiResponse, MockComparisonResponse, MockHttpResponseData } from '../types/test-types.js';
2
+
3
+ export class TestDataGenerators {
4
+ static createBase64Image(variant: 'small' | 'medium' | 'large' = 'small'): string {
5
+ // Different sized images for more realistic testing
6
+ const images = {
7
+ small: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
8
+ medium: 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
9
+ large: 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAOklEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJgggAAAABJRU5ErkJgggAAAABJRU5ErkJggg=='
10
+ };
11
+ return `data:image/png;base64,${images[variant]}`;
12
+ }
13
+
14
+ static createMockImageBuffer(size: number = 1024): Buffer {
15
+ // Create buffer with specified size for more realistic testing
16
+ const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
17
+ const buffer = Buffer.from(base64, 'base64');
18
+ // Pad buffer to reach desired size
19
+ return size > buffer.length ? Buffer.concat([buffer, Buffer.alloc(size - buffer.length)]) : buffer;
20
+ }
21
+
22
+ static createMockVideoFile(duration: number = 10): string {
23
+ // Mock MP4 file path with metadata
24
+ return `/tmp/test-video-${duration}s.mp4`;
25
+ }
26
+
27
+ static createMockGifFile(frames: number = 5): string {
28
+ // Mock GIF file path with metadata
29
+ return `/tmp/test-animation-${frames}frames.gif`;
30
+ }
31
+
32
+ static createMockAnalysisRequest(overrides: Partial<MockAnalysisRequest> = {}): MockAnalysisRequest {
33
+ const prompts = [
34
+ 'Analyze the user interface elements and their layout',
35
+ 'Focus on accessibility and usability issues',
36
+ 'Identify any visual bugs or rendering problems',
37
+ 'Review the overall design consistency',
38
+ 'Check for mobile responsiveness indicators'
39
+ ];
40
+
41
+ const selectedPrompt = prompts[Math.floor(Math.random() * prompts.length)];
42
+
43
+ return {
44
+ input: TestDataGenerators.createBase64Image(),
45
+ detail_level: Math.random() > 0.5 ? 'detailed' : 'quick',
46
+ custom_prompt: selectedPrompt,
47
+ ...overrides
48
+ };
49
+ }
50
+
51
+ static createMockCompareRequest(overrides: Partial<MockCompareRequest> = {}): MockCompareRequest {
52
+ const prompts = [
53
+ 'Compare the visual differences between these two UI states',
54
+ 'Focus on layout and structural changes',
55
+ 'Identify pixel-level differences and their impact',
56
+ 'Compare accessibility features between versions',
57
+ 'Analyze the user experience implications of changes'
58
+ ];
59
+
60
+ const comparisonTypes: Array<'pixel' | 'structural' | 'semantic'> = ['pixel', 'structural', 'semantic'];
61
+ const selectedPrompt = prompts[Math.floor(Math.random() * prompts.length)];
62
+ const selectedType = comparisonTypes[Math.floor(Math.random() * comparisonTypes.length)];
63
+
64
+ const baseRequest = {
65
+ input1: TestDataGenerators.createBase64Image('medium'),
66
+ input2: TestDataGenerators.createBase64Image('medium'),
67
+ comparison_type: selectedType as 'pixel' | 'structural' | 'semantic',
68
+ custom_prompt: selectedPrompt
69
+ };
70
+
71
+ return Object.assign({}, baseRequest, overrides) as MockCompareRequest;
72
+ }
73
+
74
+ static createMockGeminiResponse(overrides: Partial<MockGeminiResponse> = {}): MockGeminiResponse {
75
+ const responses = [
76
+ {
77
+ summary: 'Screenshot shows a web application interface',
78
+ details: 'This image contains a modern web application with a navigation bar, sidebar, and main content area. The interface uses a clean design with blue accents.',
79
+ technical_details: {
80
+ dimensions: '1920x1080',
81
+ format: 'PNG',
82
+ colors: 'full color',
83
+ ui_elements: 'navigation, sidebar, content area'
84
+ },
85
+ confidence: 0.92,
86
+ recommendations: ['Consider improving color contrast', 'Add loading states for better UX']
87
+ },
88
+ {
89
+ summary: 'Mobile app screenshot with user interface elements',
90
+ details: 'This is a mobile application screenshot showing a login form with input fields and buttons. The design follows modern mobile UI patterns.',
91
+ technical_details: {
92
+ dimensions: '375x812',
93
+ format: 'JPEG',
94
+ colors: 'full color',
95
+ platform: 'mobile'
96
+ },
97
+ confidence: 0.88,
98
+ recommendations: ['Optimize for smaller screen sizes', 'Ensure touch targets are adequate']
99
+ },
100
+ {
101
+ summary: 'Code editor interface with syntax highlighting',
102
+ details: 'The image shows a code editor with syntax highlighting, line numbers, and a file tree. Multiple tabs are open showing different files.',
103
+ technical_details: {
104
+ dimensions: '1440x900',
105
+ format: 'PNG',
106
+ colors: 'dark theme',
107
+ editor: 'VS Code-like interface'
108
+ },
109
+ confidence: 0.96,
110
+ recommendations: ['Good use of syntax highlighting', 'Consider font size for readability']
111
+ }
112
+ ];
113
+
114
+ const selectedResponse = responses[Math.floor(Math.random() * responses.length)];
115
+ return Object.assign({}, selectedResponse, overrides) as MockGeminiResponse;
116
+ }
117
+
118
+ static createMockComparisonResponse(overrides: Partial<MockComparisonResponse> = {}): MockComparisonResponse {
119
+ const responses = [
120
+ {
121
+ summary: 'Significant UI differences detected',
122
+ differences: [
123
+ 'Button color changed from blue to green',
124
+ 'Navigation bar height increased by 10px',
125
+ 'New search icon added in header'
126
+ ],
127
+ similarity_score: 0.73,
128
+ analysis_method: 'semantic',
129
+ recommendations: [
130
+ 'Review color accessibility standards',
131
+ 'Test navigation changes with users',
132
+ 'Ensure search functionality is intuitive'
133
+ ],
134
+ technical_details: {
135
+ image1_format: 'PNG',
136
+ image2_format: 'PNG',
137
+ comparison_method: 'semantic'
138
+ }
139
+ },
140
+ {
141
+ summary: 'Minor layout adjustments found',
142
+ differences: [
143
+ 'Slight margin increase in content area',
144
+ 'Font size reduced by 1px'
145
+ ],
146
+ similarity_score: 0.91,
147
+ analysis_method: 'structural',
148
+ recommendations: [
149
+ 'Changes are minimal and unlikely to impact users',
150
+ 'Consider A/B testing for optimal spacing'
151
+ ],
152
+ technical_details: {
153
+ image1_format: 'JPEG',
154
+ image2_format: 'PNG',
155
+ comparison_method: 'structural'
156
+ }
157
+ },
158
+ {
159
+ summary: 'Images are nearly identical',
160
+ differences: [],
161
+ similarity_score: 0.98,
162
+ analysis_method: 'pixel',
163
+ recommendations: ['No significant changes detected'],
164
+ technical_details: {
165
+ image1_format: 'PNG',
166
+ image2_format: 'PNG',
167
+ comparison_method: 'pixel'
168
+ }
169
+ }
170
+ ];
171
+
172
+ const selectedResponse = responses[Math.floor(Math.random() * responses.length)];
173
+ return Object.assign({}, selectedResponse, overrides) as MockComparisonResponse;
174
+ }
175
+
176
+ static createMockFileStats() {
177
+ return {
178
+ isFile: () => true,
179
+ isDirectory: () => false,
180
+ size: 1024,
181
+ mtime: new Date(),
182
+ ctime: new Date()
183
+ };
184
+ }
185
+
186
+ static createMockHttpResponse(data: MockHttpResponseData, status = 200, headers: Record<string, string> = {}) {
187
+ return new Response(typeof data === 'string' ? data : JSON.stringify(data), {
188
+ status,
189
+ headers: {
190
+ 'Content-Type': 'application/json',
191
+ ...headers
192
+ }
193
+ });
194
+ }
195
+
196
+ static createMockErrorResponse(message: string, status = 500) {
197
+ return new Response(JSON.stringify({ error: message }), {
198
+ status,
199
+ headers: { 'Content-Type': 'application/json' }
200
+ });
201
+ }
202
+
203
+ static generateRandomPort(): number {
204
+ return 3000 + Math.floor(Math.random() * 1000);
205
+ }
206
+
207
+ static createMockSessionData() {
208
+ return {
209
+ id: 'test-session-123',
210
+ created: Date.now(),
211
+ lastActivity: Date.now(),
212
+ data: {}
213
+ };
214
+ }
215
+ }
216
+
217
+ export default TestDataGenerators;