@flink-app/test-utils 1.0.0 → 2.0.0-alpha.49

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.
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.mockTool = mockTool;
40
+ /**
41
+ * Creates a mock tool with tracking and canned responses
42
+ *
43
+ * Features:
44
+ * - Simple canned responses
45
+ * - Error simulation
46
+ * - Custom function support
47
+ * - Automatic invocation tracking
48
+ * - Validation helpers
49
+ *
50
+ * @example
51
+ * // Simple canned response
52
+ * const weatherTool = mockTool({
53
+ * name: "get_weather",
54
+ * inputSchema: z.object({ city: z.string() }),
55
+ * response: { temperature: 22, conditions: "sunny" }
56
+ * });
57
+ *
58
+ * @example
59
+ * // Custom function with tracking
60
+ * const calculatorTool = mockTool({
61
+ * name: "calculate",
62
+ * inputSchema: z.object({ a: z.number(), b: z.number() }),
63
+ * fn: async ({ input }) => ({
64
+ * success: true,
65
+ * data: { result: input.a + input.b }
66
+ * })
67
+ * });
68
+ *
69
+ * @example
70
+ * // Error simulation
71
+ * const failingTool = mockTool({
72
+ * name: "fail",
73
+ * inputSchema: z.object({}),
74
+ * error: { error: "Tool failed", code: "MOCK_ERROR" }
75
+ * });
76
+ */
77
+ function mockTool(config) {
78
+ var _this = this;
79
+ var invocations = [];
80
+ // Create the tool props
81
+ var props = {
82
+ id: config.name,
83
+ description: config.description || "Mock tool: ".concat(config.name),
84
+ inputSchema: config.inputSchema,
85
+ outputSchema: config.outputSchema,
86
+ permissions: config.permissions,
87
+ };
88
+ // Create the tool function with invocation tracking
89
+ var fn = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
90
+ var input = _b.input, ctx = _b.ctx, user = _b.user;
91
+ return __generator(this, function (_c) {
92
+ // Track invocation
93
+ invocations.push({ input: input, user: user });
94
+ // If custom function provided, use it
95
+ if (config.fn) {
96
+ return [2 /*return*/, config.fn({ input: input, ctx: ctx, user: user })];
97
+ }
98
+ // If error configured, return error
99
+ if (config.error) {
100
+ return [2 /*return*/, {
101
+ success: false,
102
+ error: config.error.error,
103
+ code: config.error.code,
104
+ }];
105
+ }
106
+ // If response configured, return success with data
107
+ if (config.response !== undefined) {
108
+ return [2 /*return*/, {
109
+ success: true,
110
+ data: config.response,
111
+ }];
112
+ }
113
+ // Default: return empty success
114
+ return [2 /*return*/, {
115
+ success: true,
116
+ data: {},
117
+ }];
118
+ });
119
+ }); };
120
+ var getLastInvocation = function () {
121
+ return invocations[invocations.length - 1];
122
+ };
123
+ var reset = function () {
124
+ invocations.length = 0;
125
+ };
126
+ return {
127
+ props: props,
128
+ fn: fn,
129
+ invocations: invocations,
130
+ getLastInvocation: getLastInvocation,
131
+ reset: reset,
132
+ };
133
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./http";
2
2
  export * from "./mocks";
3
+ export * from "./ai";
package/dist/index.js CHANGED
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./http"), exports);
18
18
  __exportStar(require("./mocks"), exports);
19
+ __exportStar(require("./ai"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/test-utils",
3
- "version": "1.0.0",
3
+ "version": "2.0.0-alpha.49",
4
4
  "description": "Test utils for Flink",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
@@ -11,20 +11,24 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "got": "^9.6.0",
14
- "qs": "^6.7.0"
14
+ "qs": "^6.7.0",
15
+ "zod": "^4.3.6"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/got": "^9.6.12",
19
+ "@types/jasmine": "^3.7.1",
18
20
  "@types/node": "22.13.10",
19
21
  "@types/qs": "^6.9.7",
22
+ "jasmine": "^3.10.0",
23
+ "jasmine-ts": "^0.3.3",
20
24
  "ts-node": "^10.9.2",
21
25
  "tsc-watch": "^4.2.9",
22
- "@flink-app/flink": "1.0.0"
26
+ "@flink-app/flink": "2.0.0-alpha.49"
23
27
  },
24
28
  "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
25
29
  "scripts": {
26
- "test": "echo \"Error: no test specified\"",
27
- "build": "tsc",
30
+ "test": "jasmine-ts --config=./spec/support/jasmine.json",
31
+ "build": "tsc --project tsconfig.dist.json",
28
32
  "watch": "tsc-watch",
29
33
  "clean": "rimraf dist .flink"
30
34
  }
@@ -0,0 +1,280 @@
1
+ import { createConversation } from "../../src/ai/conversation";
2
+
3
+ describe("createConversation", () => {
4
+ describe("user messages", () => {
5
+ it("should add user message", () => {
6
+ const conversation = createConversation().user("Hello").build();
7
+
8
+ expect(conversation.length).toBe(1);
9
+ expect(conversation[0]).toEqual({
10
+ role: "user",
11
+ content: "Hello",
12
+ });
13
+ });
14
+
15
+ it("should add multiple user messages", () => {
16
+ const conversation = createConversation()
17
+ .user("First message")
18
+ .user("Second message")
19
+ .build();
20
+
21
+ expect(conversation.length).toBe(2);
22
+ expect(conversation[0]).toEqual({
23
+ role: "user",
24
+ content: "First message",
25
+ });
26
+ expect(conversation[1]).toEqual({
27
+ role: "user",
28
+ content: "Second message",
29
+ });
30
+ });
31
+ });
32
+
33
+ describe("assistant messages", () => {
34
+ it("should add assistant message without tool calls", () => {
35
+ const conversation = createConversation()
36
+ .assistant("I can help with that")
37
+ .build();
38
+
39
+ expect(conversation.length).toBe(1);
40
+ expect(conversation[0]).toEqual({
41
+ role: "assistant",
42
+ content: "I can help with that",
43
+ toolCalls: undefined,
44
+ });
45
+ });
46
+
47
+ it("should add assistant message with tool calls", () => {
48
+ const conversation = createConversation()
49
+ .assistant("Let me check", [
50
+ { id: "1", name: "get_weather", input: { city: "Stockholm" } },
51
+ ])
52
+ .build();
53
+
54
+ expect(conversation.length).toBe(1);
55
+ expect(conversation[0]).toEqual({
56
+ role: "assistant",
57
+ content: "Let me check",
58
+ toolCalls: [
59
+ { id: "1", name: "get_weather", input: { city: "Stockholm" } },
60
+ ],
61
+ });
62
+ });
63
+
64
+ it("should add multiple assistant messages", () => {
65
+ const conversation = createConversation()
66
+ .assistant("First response")
67
+ .assistant("Second response")
68
+ .build();
69
+
70
+ expect(conversation.length).toBe(2);
71
+ expect(conversation[0].role).toBe("assistant");
72
+ expect(conversation[1].role).toBe("assistant");
73
+ });
74
+ });
75
+
76
+ describe("tool messages", () => {
77
+ it("should add tool message", () => {
78
+ const conversation = createConversation()
79
+ .tool("1", "get_weather", JSON.stringify({ temp: 22 }))
80
+ .build();
81
+
82
+ expect(conversation.length).toBe(1);
83
+ expect(conversation[0]).toEqual({
84
+ role: "tool",
85
+ toolCallId: "1",
86
+ toolName: "get_weather",
87
+ result: JSON.stringify({ temp: 22 }),
88
+ });
89
+ });
90
+
91
+ it("should add multiple tool messages", () => {
92
+ const conversation = createConversation()
93
+ .tool("1", "tool_one", "result 1")
94
+ .tool("2", "tool_two", "result 2")
95
+ .build();
96
+
97
+ expect(conversation.length).toBe(2);
98
+ expect(conversation[0].role).toBe("tool");
99
+ expect(conversation[1].role).toBe("tool");
100
+ });
101
+ });
102
+
103
+ describe("fluent API", () => {
104
+ it("should chain different message types", () => {
105
+ const conversation = createConversation()
106
+ .user("What's the weather?")
107
+ .assistant("Let me check", [
108
+ { id: "1", name: "get_weather", input: { city: "Stockholm" } },
109
+ ])
110
+ .tool("1", "get_weather", JSON.stringify({ temp: 22 }))
111
+ .assistant("It's 22°C in Stockholm")
112
+ .build();
113
+
114
+ expect(conversation.length).toBe(4);
115
+ expect(conversation[0].role).toBe("user");
116
+ expect(conversation[1].role).toBe("assistant");
117
+ expect(conversation[2].role).toBe("tool");
118
+ expect(conversation[3].role).toBe("assistant");
119
+ });
120
+
121
+ it("should return builder instance for chaining", () => {
122
+ const builder = createConversation();
123
+ const result1 = builder.user("test");
124
+ const result2 = result1.assistant("response");
125
+ const result3 = result2.tool("1", "tool", "result");
126
+
127
+ expect(result1).toBe(builder);
128
+ expect(result2).toBe(builder);
129
+ expect(result3).toBe(builder);
130
+ });
131
+ });
132
+
133
+ describe("multi-turn conversations", () => {
134
+ it("should build complex multi-turn conversation", () => {
135
+ const conversation = createConversation()
136
+ .user("Book a flight to Paris")
137
+ .assistant("When would you like to travel?")
138
+ .user("Next Monday")
139
+ .assistant("Let me search for flights", [
140
+ {
141
+ id: "search_1",
142
+ name: "search_flights",
143
+ input: { destination: "Paris", date: "next Monday" },
144
+ },
145
+ ])
146
+ .tool(
147
+ "search_1",
148
+ "search_flights",
149
+ JSON.stringify({ flights: ["AF123", "BA456"] })
150
+ )
151
+ .assistant("I found these flights: AF123 and BA456")
152
+ .user("Book AF123")
153
+ .assistant("Booking flight AF123", [
154
+ { id: "book_1", name: "book_flight", input: { flightId: "AF123" } },
155
+ ])
156
+ .tool("book_1", "book_flight", JSON.stringify({ success: true }))
157
+ .assistant("Your flight has been booked!")
158
+ .build();
159
+
160
+ expect(conversation.length).toBe(10);
161
+
162
+ // Verify structure
163
+ expect(conversation[0]).toEqual({
164
+ role: "user",
165
+ content: "Book a flight to Paris",
166
+ });
167
+ expect(conversation[1]).toEqual({
168
+ role: "assistant",
169
+ content: "When would you like to travel?",
170
+ toolCalls: undefined,
171
+ });
172
+ expect(conversation[9]).toEqual({
173
+ role: "assistant",
174
+ content: "Your flight has been booked!",
175
+ toolCalls: undefined,
176
+ });
177
+ });
178
+ });
179
+
180
+ describe("edge cases", () => {
181
+ it("should build empty conversation", () => {
182
+ const conversation = createConversation().build();
183
+
184
+ expect(conversation).toEqual([]);
185
+ });
186
+
187
+ it("should handle empty strings", () => {
188
+ const conversation = createConversation()
189
+ .user("")
190
+ .assistant("")
191
+ .tool("1", "test", "")
192
+ .build();
193
+
194
+ expect(conversation.length).toBe(3);
195
+ expect((conversation[0] as any).content).toBe("");
196
+ expect((conversation[1] as any).content).toBe("");
197
+ expect((conversation[2] as any).result).toBe("");
198
+ });
199
+
200
+ it("should handle special characters in content", () => {
201
+ const specialContent = 'Hello "world" with \n newlines and \t tabs';
202
+ const conversation = createConversation()
203
+ .user(specialContent)
204
+ .assistant(specialContent)
205
+ .build();
206
+
207
+ expect((conversation[0] as any).content).toBe(specialContent);
208
+ expect((conversation[1] as any).content).toBe(specialContent);
209
+ });
210
+
211
+ it("should handle complex tool inputs", () => {
212
+ const conversation = createConversation()
213
+ .assistant("Processing", [
214
+ {
215
+ id: "complex_1",
216
+ name: "complex_tool",
217
+ input: {
218
+ nested: {
219
+ array: [1, 2, 3],
220
+ object: { key: "value" },
221
+ },
222
+ special: 'chars "quotes" \n newlines',
223
+ },
224
+ },
225
+ ])
226
+ .build();
227
+
228
+ const toolCall = (conversation[0] as any).toolCalls[0];
229
+ expect(toolCall.input.nested.array).toEqual([1, 2, 3]);
230
+ expect(toolCall.input.special).toContain("quotes");
231
+ });
232
+ });
233
+
234
+ describe("real-world usage patterns", () => {
235
+ it("should support weather agent conversation", () => {
236
+ const conversation = createConversation()
237
+ .user("What's the weather in Stockholm?")
238
+ .assistant("Let me check the weather for you", [
239
+ { id: "w1", name: "get_weather", input: { city: "Stockholm" } },
240
+ ])
241
+ .tool(
242
+ "w1",
243
+ "get_weather",
244
+ JSON.stringify({ temperature: 22, conditions: "sunny" })
245
+ )
246
+ .assistant("It's currently 22°C and sunny in Stockholm")
247
+ .build();
248
+
249
+ expect(conversation.length).toBe(4);
250
+ });
251
+
252
+ it("should support calculator agent conversation", () => {
253
+ const conversation = createConversation()
254
+ .user("What is 15 + 27?")
255
+ .assistant("Let me calculate that", [
256
+ { id: "c1", name: "calculate", input: { a: 15, b: 27 } },
257
+ ])
258
+ .tool("c1", "calculate", JSON.stringify({ result: 42 }))
259
+ .assistant("15 + 27 = 42")
260
+ .build();
261
+
262
+ expect(conversation.length).toBe(4);
263
+ });
264
+
265
+ it("should support multi-tool agent conversation", () => {
266
+ const conversation = createConversation()
267
+ .user("Get me the weather and news for Stockholm")
268
+ .assistant("I'll fetch both for you", [
269
+ { id: "w1", name: "get_weather", input: { city: "Stockholm" } },
270
+ { id: "n1", name: "get_news", input: { city: "Stockholm" } },
271
+ ])
272
+ .tool("w1", "get_weather", JSON.stringify({ temp: 22 }))
273
+ .tool("n1", "get_news", JSON.stringify({ headlines: ["News 1"] }))
274
+ .assistant("Weather: 22°C. Top news: News 1")
275
+ .build();
276
+
277
+ expect(conversation.length).toBe(5);
278
+ });
279
+ });
280
+ });