nosible 0.1.5

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 (44) hide show
  1. package/README.md +438 -0
  2. package/dist/index.cjs +1841 -0
  3. package/dist/index.cjs.map +29 -0
  4. package/dist/index.js +1815 -0
  5. package/dist/index.js.map +29 -0
  6. package/package.json +63 -0
  7. package/src/api/api.test.ts +366 -0
  8. package/src/api/index.ts +179 -0
  9. package/src/api/schemas.ts +152 -0
  10. package/src/client.test.ts +685 -0
  11. package/src/client.ts +762 -0
  12. package/src/index.ts +4 -0
  13. package/src/scrape/types.ts +119 -0
  14. package/src/scrape/webPageData.test.ts +302 -0
  15. package/src/scrape/webPageData.ts +103 -0
  16. package/src/search/analyze.test.ts +396 -0
  17. package/src/search/analyze.ts +151 -0
  18. package/src/search/bulkSearch.ts +62 -0
  19. package/src/search/result.test.ts +423 -0
  20. package/src/search/result.ts +391 -0
  21. package/src/search/result.types.ts +32 -0
  22. package/src/search/resultFactory.ts +21 -0
  23. package/src/search/resultSet.io.test.ts +320 -0
  24. package/src/search/resultSet.test.ts +368 -0
  25. package/src/search/resultSet.ts +387 -0
  26. package/src/search/resultSet.types.ts +3 -0
  27. package/src/search/search.test.ts +299 -0
  28. package/src/search/search.ts +187 -0
  29. package/src/search/searchSet.io.test.ts +321 -0
  30. package/src/search/searchSet.ts +122 -0
  31. package/src/search/sqlFilter.test.ts +129 -0
  32. package/src/search/sqlFilter.ts +147 -0
  33. package/src/test-utils/mocks.ts +159 -0
  34. package/src/topicTrend/topicTrend.ts +53 -0
  35. package/src/utils/browser.test.ts +209 -0
  36. package/src/utils/browser.ts +21 -0
  37. package/src/utils/fernet.ts +47 -0
  38. package/src/utils/file.test.ts +81 -0
  39. package/src/utils/file.ts +195 -0
  40. package/src/utils/index.ts +7 -0
  41. package/src/utils/llm.test.ts +279 -0
  42. package/src/utils/llm.ts +244 -0
  43. package/src/utils/userPlan.test.ts +332 -0
  44. package/src/utils/userPlan.ts +211 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Downloads a blob in the browser by creating a temporary link and triggering a download
3
+ * @param data - The data string to download
4
+ * @param mimeType - The MIME type of the file (e.g., 'application/json', 'text/csv')
5
+ * @param fileName - The name of the file to download
6
+ */
7
+ export const downloadBlob = (
8
+ data: string,
9
+ mimeType: string,
10
+ fileName: string
11
+ ): void => {
12
+ const blob = new Blob([data], {type: mimeType});
13
+ const url = URL.createObjectURL(blob);
14
+ const a = document.createElement("a");
15
+ a.href = url;
16
+ a.download = fileName;
17
+ document.body.appendChild(a);
18
+ a.click();
19
+ document.body.removeChild(a);
20
+ URL.revokeObjectURL(url);
21
+ };
@@ -0,0 +1,47 @@
1
+ import {createDecipheriv, createHmac, timingSafeEqual} from "crypto";
2
+
3
+ /**
4
+ * Decrypt data using Fernet-compatible decryption
5
+ * Fernet uses: version (1 byte) | timestamp (8 bytes) | iv (16 bytes) | ciphertext | hmac (32 bytes)
6
+ */
7
+ export const fernetDecrypt = (token: Buffer, key: string): Buffer => {
8
+ try {
9
+ // Decode the base64 key and derive encryption and signing keys
10
+ const keyBuffer = Buffer.from(key, "base64");
11
+ const signingKey = keyBuffer.subarray(0, 16);
12
+ const encryptionKey = keyBuffer.subarray(16, 32);
13
+
14
+ // Verify HMAC (last 32 bytes)
15
+ const hmac = token.subarray(token.length - 32);
16
+ const payload = token.subarray(0, token.length - 32);
17
+
18
+ const expectedHmac = createHmac("sha256", signingKey)
19
+ .update(payload)
20
+ .digest();
21
+
22
+ if (!timingSafeEqual(hmac, expectedHmac)) {
23
+ throw new Error("HMAC verification failed");
24
+ }
25
+
26
+ // Extract components
27
+ const version = payload[0];
28
+ if (version !== 0x80) {
29
+ throw new Error(`Unsupported Fernet version: ${version}`);
30
+ }
31
+
32
+ const iv = payload.subarray(9, 25); // Skip version (1) + timestamp (8)
33
+ const ciphertext = payload.subarray(25);
34
+
35
+ // Decrypt using AES-128-CBC
36
+ const decipher = createDecipheriv("aes-128-cbc", encryptionKey, iv);
37
+ const decrypted = Buffer.concat([
38
+ decipher.update(ciphertext),
39
+ decipher.final(),
40
+ ]);
41
+
42
+ return decrypted;
43
+ } catch (error) {
44
+ console.error("Error decrypting data:", error);
45
+ throw error;
46
+ }
47
+ };
@@ -0,0 +1,81 @@
1
+ import {describe, test, expect, afterEach} from "bun:test";
2
+ import {importCsv, exportCsv} from "./file";
3
+ import {writeFile, unlink} from "node:fs/promises";
4
+
5
+ describe("importCsv", () => {
6
+ const testFiles: string[] = [];
7
+
8
+ afterEach(async () => {
9
+ // Clean up test files
10
+ for (const file of testFiles) {
11
+ try {
12
+ await unlink(file);
13
+ } catch (error) {
14
+ // Ignore errors if file doesn't exist
15
+ }
16
+ }
17
+ testFiles.length = 0; // Clear the array
18
+ });
19
+
20
+ test("should import CSV data from file path", async () => {
21
+ const csvContent = `name,age,city
22
+ John Doe,30,New York
23
+ Jane Smith,25,Los Angeles
24
+ Bob Johnson,35,Chicago`;
25
+
26
+ const testFilePath = "/tmp/test-import.csv";
27
+ testFiles.push(testFilePath);
28
+
29
+ await writeFile(testFilePath, csvContent);
30
+
31
+ const result = await importCsv({filePath: testFilePath});
32
+
33
+ expect(result).toEqual([
34
+ {name: "John Doe", age: 30, city: "New York"},
35
+ {name: "Jane Smith", age: 25, city: "Los Angeles"},
36
+ {name: "Bob Johnson", age: 35, city: "Chicago"},
37
+ ]);
38
+ });
39
+
40
+ test("should handle empty CSV file", async () => {
41
+ const testFilePath = "/tmp/test-empty.csv";
42
+ testFiles.push(testFilePath);
43
+
44
+ await writeFile(testFilePath, "");
45
+
46
+ const result = await importCsv({filePath: testFilePath});
47
+
48
+ expect(result).toEqual([]);
49
+ });
50
+
51
+ test("should handle CSV with only headers", async () => {
52
+ const csvContent = "name,age,city";
53
+ const testFilePath = "/tmp/test-headers-only.csv";
54
+ testFiles.push(testFilePath);
55
+
56
+ await writeFile(testFilePath, csvContent);
57
+
58
+ const result = await importCsv({filePath: testFilePath});
59
+
60
+ expect(result).toEqual([]);
61
+ });
62
+
63
+ test("should round trip CSV data", async () => {
64
+ const testData = [
65
+ {name: "Alice", age: 28, city: "Boston"},
66
+ {name: "Charlie", age: 32, city: "Seattle"},
67
+ ];
68
+
69
+ const exportPath = "/tmp/test-roundtrip.csv";
70
+ testFiles.push(exportPath);
71
+
72
+ // Export to CSV
73
+ await exportCsv(testData, exportPath);
74
+
75
+ // Import from CSV
76
+ const importedData = await importCsv({filePath: exportPath});
77
+
78
+ // Data should match (note: papaparse converts numbers to numbers)
79
+ expect(importedData).toEqual(testData);
80
+ });
81
+ });
@@ -0,0 +1,195 @@
1
+ import {isBrowser} from ".";
2
+ import {downloadBlob} from "./browser";
3
+ import Papa from "papaparse";
4
+
5
+ /**
6
+ * Exports data to a JSON file
7
+ * @param data - The data to export
8
+ * @param outputPath - Path to write the JSON file (or filename for browser)
9
+ */
10
+ export const exportJson = async (
11
+ data: any,
12
+ outputPath: string
13
+ ): Promise<void> => {
14
+ try {
15
+ if (isBrowser()) {
16
+ // Extract filename from outputPath or use default
17
+ const filename = outputPath.split("/").pop() || "export.json";
18
+ const jsonString = JSON.stringify(data);
19
+ const finalFilename = filename.endsWith(".json")
20
+ ? filename
21
+ : `${filename}.json`;
22
+
23
+ downloadBlob(jsonString, "application/json", finalFilename);
24
+ } else {
25
+ const {writeFile} = await import("node:fs/promises");
26
+ await writeFile(outputPath, JSON.stringify(data));
27
+ }
28
+ } catch (error) {
29
+ const errorMessage = error instanceof Error ? error.message : String(error);
30
+ throw new Error(`Failed to write JSON: ${errorMessage}`);
31
+ }
32
+ };
33
+
34
+ /**
35
+ * Imports JSON data from a file
36
+ * @param options - Either {filePath: string} for server-side or {file: File} for browser-side
37
+ * @returns Parsed JSON data
38
+ */
39
+ export const importJson = async (
40
+ options: {filePath: string} | {file: File}
41
+ ): Promise<any> => {
42
+ try {
43
+ if ("file" in options) {
44
+ // Browser-side: read from File object
45
+ const text = await options.file.text();
46
+ return JSON.parse(text);
47
+ } else {
48
+ // Server-side: read from file path
49
+ if (isBrowser()) {
50
+ throw new Error(
51
+ "File path import is not supported in browser. Use {file: File} instead."
52
+ );
53
+ }
54
+ const {readFile} = await import("node:fs/promises");
55
+ const data = await readFile(options.filePath, "utf-8");
56
+ return JSON.parse(data);
57
+ }
58
+ } catch (error) {
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`Failed to read JSON: ${errorMessage}`);
61
+ }
62
+ };
63
+
64
+ /**
65
+ * Imports CSV data from a file
66
+ * @param options - Either {filePath: string} for server-side or {file: File} for browser-side
67
+ * @returns Parsed CSV data as an array of objects
68
+ */
69
+ export const importCsv = async (
70
+ options: {filePath: string} | {file: File}
71
+ ): Promise<any[]> => {
72
+ try {
73
+ let csvText: string;
74
+
75
+ if ("file" in options) {
76
+ // Browser-side: read from File object
77
+ csvText = await options.file.text();
78
+ } else {
79
+ // Server-side: read from file path
80
+ if (isBrowser()) {
81
+ throw new Error(
82
+ "File path import is not supported in browser. Use {file: File} instead."
83
+ );
84
+ }
85
+ const {readFile} = await import("node:fs/promises");
86
+ csvText = await readFile(options.filePath, "utf-8");
87
+ }
88
+
89
+ // Handle empty CSV
90
+ if (!csvText.trim()) {
91
+ return [];
92
+ }
93
+
94
+ // Parse CSV using papaparse
95
+ const result = Papa.parse(csvText, {
96
+ header: true,
97
+ skipEmptyLines: true,
98
+ dynamicTyping: true,
99
+ });
100
+
101
+ // Filter out delimiter detection errors for empty/whitespace-only content
102
+ const significantErrors = result.errors.filter(
103
+ (error) =>
104
+ !error.message.includes("Unable to auto-detect delimiting character")
105
+ );
106
+
107
+ if (significantErrors.length > 0) {
108
+ const errorMessages = significantErrors
109
+ .map((err) => err.message)
110
+ .join(", ");
111
+ throw new Error(`CSV parsing errors: ${errorMessages}`);
112
+ }
113
+
114
+ return result.data as any[];
115
+ } catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ throw new Error(`Failed to read CSV: ${errorMessage}`);
118
+ }
119
+ };
120
+
121
+ /**
122
+ * Exports data to an NDJSON file (newline-delimited JSON)
123
+ * @param data - Array of objects to export
124
+ * @param outputPath - Path to write the NDJSON file (or filename for browser)
125
+ */
126
+ export const exportNdjson = async (
127
+ data: any[],
128
+ outputPath: string
129
+ ): Promise<void> => {
130
+ try {
131
+ if (isBrowser()) {
132
+ // Extract filename from outputPath or use default
133
+ const filename = outputPath.split("/").pop() || "export.ndjson";
134
+
135
+ // Create NDJSON string (one JSON object per line)
136
+ const ndjsonString = data.map((item) => JSON.stringify(item)).join("\n");
137
+
138
+ const finalFilename = filename.endsWith(".ndjson")
139
+ ? filename
140
+ : `${filename}.ndjson`;
141
+
142
+ downloadBlob(ndjsonString, "application/x-ndjson", finalFilename);
143
+ } else {
144
+ const pl = await import("nodejs-polars");
145
+ const df = pl.DataFrame(data);
146
+ await df.writeJSON(outputPath, {format: "lines"});
147
+ }
148
+ } catch (error) {
149
+ const errorMessage = error instanceof Error ? error.message : String(error);
150
+ throw new Error(`Failed to write NDJSON: ${errorMessage}`);
151
+ }
152
+ };
153
+
154
+ /**
155
+ * Exports data to a CSV file
156
+ * @param data - Array of objects to export
157
+ * @param outputPath - Path to write the CSV file (or filename for browser)
158
+ */
159
+ export const exportCsv = async (
160
+ data: any[],
161
+ outputPath: string
162
+ ): Promise<void> => {
163
+ try {
164
+ // Validate input
165
+ if (!Array.isArray(data) || data.length === 0) {
166
+ throw new Error("Data must be a non-empty array");
167
+ }
168
+
169
+ if (!outputPath || typeof outputPath !== "string") {
170
+ throw new Error("Output path must be a non-empty string");
171
+ }
172
+
173
+ // Use papaparse to convert data to CSV
174
+ const csvString = Papa.unparse(data, {
175
+ header: true,
176
+ newline: "\n",
177
+ });
178
+
179
+ if (isBrowser()) {
180
+ // Extract filename from outputPath or use default
181
+ const filename = outputPath.split("/").pop() || "export.csv";
182
+ const finalFilename = filename.endsWith(".csv")
183
+ ? filename
184
+ : `${filename}.csv`;
185
+
186
+ downloadBlob(csvString, "text/csv", finalFilename);
187
+ } else {
188
+ const {writeFile} = await import("node:fs/promises");
189
+ await writeFile(outputPath, csvString);
190
+ }
191
+ } catch (error) {
192
+ const errorMessage = error instanceof Error ? error.message : String(error);
193
+ throw new Error(`Failed to write CSV: ${errorMessage}`);
194
+ }
195
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Checks if the code is running in a browser environment
3
+ * @returns {boolean} True if running in a browser, false otherwise
4
+ */
5
+ export const isBrowser = () => {
6
+ return typeof window !== "undefined";
7
+ };
@@ -0,0 +1,279 @@
1
+ import {describe, test, expect, beforeAll, mock} from "bun:test";
2
+ import {getSentiment, generateExpansions} from "./llm";
3
+ import OpenAI from "openai";
4
+
5
+ // Helper to create a mock LLM client
6
+ const createMockLLMClient = (mockResponse: string): OpenAI => {
7
+ return {
8
+ chat: {
9
+ completions: {
10
+ create: mock(async () => ({
11
+ choices: [
12
+ {
13
+ message: {
14
+ content: mockResponse,
15
+ },
16
+ },
17
+ ],
18
+ })),
19
+ },
20
+ },
21
+ } as unknown as OpenAI;
22
+ };
23
+
24
+ describe("getSentiment", () => {
25
+ let realLlmClient: OpenAI;
26
+
27
+ beforeAll(() => {
28
+ const llmApiKeyToUse = process.env.LLM_API_KEY;
29
+ if (!llmApiKeyToUse) {
30
+ throw new Error("LLM_API_KEY environment variable is required for tests");
31
+ }
32
+
33
+ realLlmClient = new OpenAI({
34
+ apiKey: llmApiKeyToUse,
35
+ baseURL: "https://openrouter.ai/api/v1",
36
+ });
37
+ });
38
+
39
+ // Integration test - uses real API
40
+ test("should return positive sentiment for positive text (integration)", async () => {
41
+ const positiveText =
42
+ "I absolutely love this product! It's amazing and works perfectly. Best purchase ever!";
43
+
44
+ const score = await getSentiment({
45
+ text: positiveText,
46
+ llmClient: realLlmClient,
47
+ });
48
+
49
+ expect(score).toBeGreaterThanOrEqual(0.5);
50
+ expect(score).toBeLessThanOrEqual(1.0);
51
+ }, 30000);
52
+
53
+ // Mocked tests
54
+ test("should return negative sentiment for negative text", async () => {
55
+ const mockClient = createMockLLMClient("-0.85");
56
+ const negativeText =
57
+ "This is terrible. I hate it and it completely failed. Worst experience ever.";
58
+
59
+ const score = await getSentiment({
60
+ text: negativeText,
61
+ llmClient: mockClient,
62
+ });
63
+
64
+ expect(score).toBe(-0.85);
65
+ expect(score).toBeGreaterThanOrEqual(-1.0);
66
+ expect(score).toBeLessThanOrEqual(-0.5);
67
+ });
68
+
69
+ test("should return neutral sentiment for neutral text", async () => {
70
+ const mockClient = createMockLLMClient("0.1");
71
+ const neutralText =
72
+ "The product is a widget that performs basic functions. It has standard features.";
73
+
74
+ const score = await getSentiment({
75
+ text: neutralText,
76
+ llmClient: mockClient,
77
+ });
78
+
79
+ expect(score).toBe(0.1);
80
+ expect(score).toBeGreaterThanOrEqual(-0.5);
81
+ expect(score).toBeLessThanOrEqual(0.5);
82
+ });
83
+
84
+ test("should throw error when no client is provided", async () => {
85
+ const text = "Some test text";
86
+
87
+ await expect(
88
+ getSentiment({
89
+ text,
90
+ llmClient: undefined,
91
+ })
92
+ ).rejects.toThrow(
93
+ "A OpenAI client instance must be provided as 'llmClient'."
94
+ );
95
+ });
96
+
97
+ test("should handle empty string", async () => {
98
+ const mockClient = createMockLLMClient("0.5");
99
+ const text = "";
100
+
101
+ await expect(
102
+ getSentiment({
103
+ text,
104
+ llmClient: mockClient,
105
+ })
106
+ ).rejects.toThrow("Text must not be empty or whitespace.");
107
+ });
108
+
109
+ test("should parse valid float responses", async () => {
110
+ const mockClient = createMockLLMClient("0.75");
111
+ const text = "This is reasonably good";
112
+
113
+ const score = await getSentiment({
114
+ text,
115
+ llmClient: mockClient,
116
+ });
117
+
118
+ expect(typeof score).toBe("number");
119
+ expect(Number.isFinite(score)).toBe(true);
120
+ expect(score).toBe(0.75);
121
+ });
122
+
123
+ test("should throw error for invalid sentiment response", async () => {
124
+ const mockClient = createMockLLMClient("not a number");
125
+ const text = "Some text";
126
+
127
+ await expect(
128
+ getSentiment({
129
+ text,
130
+ llmClient: mockClient,
131
+ })
132
+ ).rejects.toThrow("Sentiment response is not a float");
133
+ });
134
+
135
+ test("should throw error for out of range sentiment", async () => {
136
+ const mockClient = createMockLLMClient("1.5");
137
+ const text = "Some text";
138
+
139
+ await expect(
140
+ getSentiment({
141
+ text,
142
+ llmClient: mockClient,
143
+ })
144
+ ).rejects.toThrow("Sentiment 1.5 outside valid range [-1.0, 1.0]");
145
+ });
146
+ });
147
+
148
+ describe("generateExpansions", () => {
149
+ let realLlmClient: OpenAI;
150
+
151
+ beforeAll(() => {
152
+ const llmApiKeyToUse = process.env.LLM_API_KEY;
153
+ if (!llmApiKeyToUse) {
154
+ throw new Error("LLM_API_KEY environment variable is required for tests");
155
+ }
156
+
157
+ realLlmClient = new OpenAI({
158
+ apiKey: llmApiKeyToUse,
159
+ baseURL: "https://openrouter.ai/api/v1",
160
+ });
161
+ });
162
+
163
+ // Integration test - uses real API
164
+ test("should return 10 expansion strings for a typical question (integration)", async () => {
165
+ const question = "What are effective treatments for type 2 diabetes?";
166
+
167
+ const expansions = await generateExpansions({
168
+ question,
169
+ llmClient: realLlmClient,
170
+ });
171
+
172
+ expect(Array.isArray(expansions)).toBe(true);
173
+ expect(expansions.length).toBe(10);
174
+ expect(expansions.every((q) => typeof q === "string")).toBe(true);
175
+ }, 30000);
176
+
177
+ // Mocked tests
178
+ test("should parse valid JSON array response", async () => {
179
+ const mockExpansions = [
180
+ "What are the best treatments for managing type 2 diabetes?",
181
+ "How can type 2 diabetes be effectively treated?",
182
+ "What medications are used to treat type 2 diabetes?",
183
+ "What are the recommended therapies for type 2 diabetes?",
184
+ "How to control type 2 diabetes with treatment?",
185
+ "What treatment options exist for type 2 diabetes patients?",
186
+ "How is type 2 diabetes managed medically?",
187
+ "What are effective interventions for type 2 diabetes?",
188
+ "Which treatments work best for type 2 diabetes?",
189
+ "What are proven methods to treat type 2 diabetes?",
190
+ ];
191
+ const mockClient = createMockLLMClient(JSON.stringify(mockExpansions));
192
+ const question = "What are effective treatments for type 2 diabetes?";
193
+
194
+ const expansions = await generateExpansions({
195
+ question,
196
+ llmClient: mockClient,
197
+ });
198
+
199
+ expect(Array.isArray(expansions)).toBe(true);
200
+ expect(expansions.length).toBe(10);
201
+ expect(expansions).toEqual(mockExpansions);
202
+ });
203
+
204
+ test("should handle JSON wrapped in markdown code blocks", async () => {
205
+ const mockExpansions = [
206
+ "Question 1",
207
+ "Question 2",
208
+ "Question 3",
209
+ "Question 4",
210
+ "Question 5",
211
+ "Question 6",
212
+ "Question 7",
213
+ "Question 8",
214
+ "Question 9",
215
+ "Question 10",
216
+ ];
217
+ const mockClient = createMockLLMClient(
218
+ `\`\`\`json\n${JSON.stringify(mockExpansions)}\n\`\`\``
219
+ );
220
+ const question = "Test question";
221
+
222
+ const expansions = await generateExpansions({
223
+ question,
224
+ llmClient: mockClient,
225
+ });
226
+
227
+ expect(expansions).toEqual(mockExpansions);
228
+ });
229
+
230
+ test("should throw error when no client is provided", async () => {
231
+ const question = "What are the symptoms of seasonal allergies?";
232
+
233
+ await expect(
234
+ generateExpansions({
235
+ question,
236
+ llmClient: undefined,
237
+ })
238
+ ).rejects.toThrow(
239
+ "A OpenAI client instance must be provided as 'llmClient'."
240
+ );
241
+ });
242
+
243
+ test("should throw on empty question", async () => {
244
+ const mockClient = createMockLLMClient("[]");
245
+
246
+ await expect(
247
+ generateExpansions({
248
+ question: "",
249
+ llmClient: mockClient,
250
+ })
251
+ ).rejects.toThrow("Question must not be empty or whitespace.");
252
+ });
253
+
254
+ test("should throw error for invalid JSON response", async () => {
255
+ const mockClient = createMockLLMClient("not valid json");
256
+ const question = "Test question";
257
+
258
+ await expect(
259
+ generateExpansions({
260
+ question,
261
+ llmClient: mockClient,
262
+ })
263
+ ).rejects.toThrow("OpenRouter response was not valid JSON");
264
+ });
265
+
266
+ test("should throw error for invalid array length", async () => {
267
+ const mockClient = createMockLLMClient(
268
+ JSON.stringify(["Question 1", "Question 2"])
269
+ );
270
+ const question = "Test question";
271
+
272
+ await expect(
273
+ generateExpansions({
274
+ question,
275
+ llmClient: mockClient,
276
+ })
277
+ ).rejects.toThrow("Invalid response");
278
+ });
279
+ });