Skip to content

Commit b87c34a

Browse files
feat: implement anti-pattern remediation from review
- Add semantic release configuration (.releaserc.js) - Implement external API mocking to reduce test flakiness - Add comprehensive mock implementations for OpenAI, Azure, Pinecone, Chroma - Create dependency injection container for IoC pattern - Add SLO monitoring system for production observability - Update Jest setup with global API mocks - Address P2 risks: inconsistent versioning and test flakiness Addresses anti-pattern review T-1 and T-2 remediation items
1 parent 2690c75 commit b87c34a

File tree

7 files changed

+1560
-0
lines changed

7 files changed

+1560
-0
lines changed

.releaserc.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
module.exports = {
2+
branches: [
3+
'main',
4+
{
5+
name: 'develop',
6+
prerelease: 'beta'
7+
}
8+
],
9+
plugins: [
10+
'@semantic-release/commit-analyzer',
11+
'@semantic-release/release-notes-generator',
12+
'@semantic-release/changelog',
13+
'@semantic-release/npm',
14+
'@semantic-release/github',
15+
[
16+
'@semantic-release/git',
17+
{
18+
assets: ['CHANGELOG.md', 'package.json'],
19+
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
20+
}
21+
]
22+
],
23+
preset: 'conventionalcommits',
24+
releaseRules: [
25+
{ type: 'feat', release: 'minor' },
26+
{ type: 'fix', release: 'patch' },
27+
{ type: 'perf', release: 'patch' },
28+
{ type: 'revert', release: 'patch' },
29+
{ type: 'docs', release: false },
30+
{ type: 'style', release: false },
31+
{ type: 'chore', release: false },
32+
{ type: 'refactor', release: 'patch' },
33+
{ type: 'test', release: false },
34+
{ type: 'build', release: false },
35+
{ type: 'ci', release: false },
36+
{ breaking: true, release: 'major' }
37+
],
38+
parserOpts: {
39+
noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES']
40+
}
41+
};

__tests__/mocks/external-apis.js

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* Mock implementations for external APIs to reduce test flakiness
3+
* Addresses P2 risk: Test Flakiness from anti-pattern review
4+
*/
5+
6+
const crypto = require('crypto');
7+
8+
/**
9+
* Mock OpenAI API responses
10+
*/
11+
class MockOpenAI {
12+
constructor() {
13+
this.apiKey = 'mock-api-key';
14+
this.baseURL = 'https://api.openai.com/v1';
15+
}
16+
17+
embeddings = {
18+
create: jest.fn().mockImplementation(async ({ input, model = 'text-embedding-ada-002' }) => {
19+
// Simulate API delay
20+
await new Promise(resolve => setTimeout(resolve, 100));
21+
22+
const inputs = Array.isArray(input) ? input : [input];
23+
const embeddings = inputs.map(() =>
24+
Array.from({ length: 1536 }, () => Math.random() * 2 - 1)
25+
);
26+
27+
return {
28+
object: 'list',
29+
data: embeddings.map((embedding, index) => ({
30+
object: 'embedding',
31+
embedding,
32+
index
33+
})),
34+
model,
35+
usage: {
36+
prompt_tokens: inputs.join(' ').split(' ').length,
37+
total_tokens: inputs.join(' ').split(' ').length
38+
}
39+
};
40+
})
41+
};
42+
43+
chat = {
44+
completions: {
45+
create: jest.fn().mockImplementation(async ({ messages, model = 'gpt-3.5-turbo', stream = false }) => {
46+
await new Promise(resolve => setTimeout(resolve, 200));
47+
48+
const response = {
49+
id: `chatcmpl-${crypto.randomUUID()}`,
50+
object: 'chat.completion',
51+
created: Math.floor(Date.now() / 1000),
52+
model,
53+
choices: [{
54+
index: 0,
55+
message: {
56+
role: 'assistant',
57+
content: `Mock response to: ${messages[messages.length - 1]?.content || 'Hello'}`
58+
},
59+
finish_reason: 'stop'
60+
}],
61+
usage: {
62+
prompt_tokens: 20,
63+
completion_tokens: 10,
64+
total_tokens: 30
65+
}
66+
};
67+
68+
if (stream) {
69+
// Mock streaming response
70+
return {
71+
async *[Symbol.asyncIterator]() {
72+
const words = response.choices[0].message.content.split(' ');
73+
for (const word of words) {
74+
yield {
75+
choices: [{
76+
delta: { content: word + ' ' },
77+
index: 0,
78+
finish_reason: null
79+
}]
80+
};
81+
await new Promise(resolve => setTimeout(resolve, 50));
82+
}
83+
yield {
84+
choices: [{
85+
delta: {},
86+
index: 0,
87+
finish_reason: 'stop'
88+
}]
89+
};
90+
}
91+
};
92+
}
93+
94+
return response;
95+
})
96+
}
97+
};
98+
}
99+
100+
/**
101+
* Mock Azure OpenAI API responses
102+
*/
103+
class MockAzureOpenAI extends MockOpenAI {
104+
constructor() {
105+
super();
106+
this.baseURL = 'https://mock-resource.openai.azure.com';
107+
this.apiVersion = '2023-12-01-preview';
108+
}
109+
}
110+
111+
/**
112+
* Mock Pinecone vector database
113+
*/
114+
class MockPinecone {
115+
constructor() {
116+
this.vectors = new Map();
117+
}
118+
119+
index(indexName) {
120+
return {
121+
upsert: jest.fn().mockImplementation(async ({ vectors }) => {
122+
vectors.forEach(vector => {
123+
this.vectors.set(vector.id, vector);
124+
});
125+
return { upsertedCount: vectors.length };
126+
}),
127+
128+
query: jest.fn().mockImplementation(async ({ vector, topK = 10, filter = {} }) => {
129+
await new Promise(resolve => setTimeout(resolve, 150));
130+
131+
// Simple mock similarity calculation
132+
const results = Array.from(this.vectors.values())
133+
.map(stored => ({
134+
id: stored.id,
135+
score: Math.random() * 0.5 + 0.5, // Mock similarity score
136+
values: stored.values,
137+
metadata: stored.metadata
138+
}))
139+
.sort((a, b) => b.score - a.score)
140+
.slice(0, topK);
141+
142+
return {
143+
matches: results,
144+
namespace: ''
145+
};
146+
}),
147+
148+
delete: jest.fn().mockImplementation(async ({ ids }) => {
149+
ids.forEach(id => this.vectors.delete(id));
150+
return {};
151+
})
152+
};
153+
}
154+
}
155+
156+
/**
157+
* Mock Chroma vector database
158+
*/
159+
class MockChroma {
160+
constructor() {
161+
this.collections = new Map();
162+
}
163+
164+
getOrCreateCollection({ name }) {
165+
if (!this.collections.has(name)) {
166+
this.collections.set(name, {
167+
name,
168+
documents: [],
169+
embeddings: [],
170+
metadatas: [],
171+
ids: []
172+
});
173+
}
174+
175+
const collection = this.collections.get(name);
176+
177+
return {
178+
add: jest.fn().mockImplementation(async ({ ids, embeddings, documents, metadatas }) => {
179+
await new Promise(resolve => setTimeout(resolve, 100));
180+
181+
collection.ids.push(...ids);
182+
collection.embeddings.push(...embeddings);
183+
collection.documents.push(...documents);
184+
collection.metadatas.push(...metadatas);
185+
186+
return {};
187+
}),
188+
189+
query: jest.fn().mockImplementation(async ({ queryEmbeddings, nResults = 10 }) => {
190+
await new Promise(resolve => setTimeout(resolve, 120));
191+
192+
const results = collection.ids.map((id, index) => ({
193+
id,
194+
distance: Math.random() * 0.5,
195+
document: collection.documents[index],
196+
metadata: collection.metadatas[index]
197+
}))
198+
.sort((a, b) => a.distance - b.distance)
199+
.slice(0, nResults);
200+
201+
return {
202+
ids: [results.map(r => r.id)],
203+
distances: [results.map(r => r.distance)],
204+
documents: [results.map(r => r.document)],
205+
metadatas: [results.map(r => r.metadata)]
206+
};
207+
})
208+
};
209+
}
210+
}
211+
212+
/**
213+
* Mock HTTP client for external API calls
214+
*/
215+
class MockHttpClient {
216+
constructor() {
217+
this.responses = new Map();
218+
}
219+
220+
setMockResponse(url, response, delay = 100) {
221+
this.responses.set(url, { response, delay });
222+
}
223+
224+
async get(url, config = {}) {
225+
const mock = this.responses.get(url);
226+
if (mock) {
227+
await new Promise(resolve => setTimeout(resolve, mock.delay));
228+
return mock.response;
229+
}
230+
231+
throw new Error(`No mock response configured for URL: ${url}`);
232+
}
233+
234+
async post(url, data, config = {}) {
235+
return this.get(url, config);
236+
}
237+
238+
async put(url, data, config = {}) {
239+
return this.get(url, config);
240+
}
241+
242+
async delete(url, config = {}) {
243+
return this.get(url, config);
244+
}
245+
}
246+
247+
/**
248+
* Network simulation utilities
249+
*/
250+
class NetworkSimulator {
251+
static simulateLatency(min = 50, max = 200) {
252+
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
253+
return new Promise(resolve => setTimeout(resolve, delay));
254+
}
255+
256+
static simulateFailure(probability = 0.1) {
257+
if (Math.random() < probability) {
258+
throw new Error('Simulated network failure');
259+
}
260+
}
261+
262+
static simulateTimeout(timeoutMs = 5000) {
263+
return new Promise((_, reject) => {
264+
setTimeout(() => reject(new Error('Request timeout')), timeoutMs);
265+
});
266+
}
267+
}
268+
269+
/**
270+
* Test utilities for mocking external dependencies
271+
*/
272+
class TestMockUtils {
273+
static createDeterministicEmbedding(text, dimensions = 1536) {
274+
// Create deterministic embedding based on text hash
275+
const hash = crypto.createHash('sha256').update(text).digest();
276+
const embedding = [];
277+
278+
for (let i = 0; i < dimensions; i++) {
279+
const byte = hash[i % hash.length];
280+
embedding.push((byte / 255) * 2 - 1); // Normalize to [-1, 1]
281+
}
282+
283+
return embedding;
284+
}
285+
286+
static mockApiResponse(data, status = 200, delay = 100) {
287+
return new Promise(resolve => {
288+
setTimeout(() => {
289+
resolve({
290+
status,
291+
data,
292+
headers: {
293+
'content-type': 'application/json',
294+
'x-ratelimit-remaining': '999'
295+
}
296+
});
297+
}, delay);
298+
});
299+
}
300+
301+
static createMockStream(chunks, chunkDelay = 50) {
302+
let index = 0;
303+
304+
return {
305+
async *[Symbol.asyncIterator]() {
306+
while (index < chunks.length) {
307+
await new Promise(resolve => setTimeout(resolve, chunkDelay));
308+
yield chunks[index++];
309+
}
310+
}
311+
};
312+
}
313+
}
314+
315+
module.exports = {
316+
MockOpenAI,
317+
MockAzureOpenAI,
318+
MockPinecone,
319+
MockChroma,
320+
MockHttpClient,
321+
NetworkSimulator,
322+
TestMockUtils
323+
};

0 commit comments

Comments
 (0)