@dbx-tools/shared-genie 0.3.36 → 0.3.39
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/package.json +7 -3
- package/test/event.test.ts +0 -695
- package/test/tsconfig.json +0 -14
- package/tsconfig.json +0 -40
package/package.json
CHANGED
|
@@ -12,21 +12,25 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"zod": "4.3.6",
|
|
15
|
-
"@dbx-tools/shared-core": "0.3.
|
|
16
|
-
"@dbx-tools/shared-sdk-model": "0.3.
|
|
15
|
+
"@dbx-tools/shared-core": "0.3.39",
|
|
16
|
+
"@dbx-tools/shared-sdk-model": "0.3.39"
|
|
17
17
|
},
|
|
18
18
|
"main": "index.ts",
|
|
19
19
|
"license": "UNLICENSED",
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
|
-
"version": "0.3.
|
|
23
|
+
"version": "0.3.39",
|
|
24
24
|
"types": "index.ts",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"exports": {
|
|
27
27
|
".": "./index.ts",
|
|
28
28
|
"./package.json": "./package.json"
|
|
29
29
|
},
|
|
30
|
+
"files": [
|
|
31
|
+
"index.ts",
|
|
32
|
+
"src"
|
|
33
|
+
],
|
|
30
34
|
"dbxToolsConfig": {
|
|
31
35
|
"tags": [
|
|
32
36
|
"shared"
|
package/test/event.test.ts
DELETED
|
@@ -1,695 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the pure event-detection layer (`event.ts`).
|
|
3
|
-
*
|
|
4
|
-
* Three concerns are covered, each in its own block:
|
|
5
|
-
*
|
|
6
|
-
* 1. The detector factory + every concrete detector. Detectors
|
|
7
|
-
* are pure functions of `(current, previous, location)` so the
|
|
8
|
-
* tests are straight value-in / value-out assertions; no I/O,
|
|
9
|
-
* no timers, no SDK mocks.
|
|
10
|
-
* 2. The `eventsFromMessage` sync generator. Verifies dispatch
|
|
11
|
-
* order, multiple-attachment fan-out, and the prev-attachment
|
|
12
|
-
* match strategy (id-based when ids exist, positional for
|
|
13
|
-
* anonymous slots).
|
|
14
|
-
* 3. The discriminated-union shape: every detector's output gets
|
|
15
|
-
* stamped with the matching `type` literal at yield time, and
|
|
16
|
-
* TypeScript narrows correctly on the `type` discriminator.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import { describe, it } from "node:test";
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
detectAttachmentAdded,
|
|
24
|
-
detectQuery,
|
|
25
|
-
detectRows,
|
|
26
|
-
detectStatement,
|
|
27
|
-
detectStatus,
|
|
28
|
-
detectSuggestedQuestions,
|
|
29
|
-
detectText,
|
|
30
|
-
detectThinking,
|
|
31
|
-
eventDetector,
|
|
32
|
-
eventsFromMessage,
|
|
33
|
-
} from "../src/event";
|
|
34
|
-
import type {
|
|
35
|
-
AttachmentEvent,
|
|
36
|
-
GenieAttachment,
|
|
37
|
-
GenieChatEvent,
|
|
38
|
-
GenieChatEventFields,
|
|
39
|
-
GenieChatLocation,
|
|
40
|
-
GenieMessage,
|
|
41
|
-
GenieThought,
|
|
42
|
-
ThinkingEvent,
|
|
43
|
-
} from "../src/genie-model";
|
|
44
|
-
|
|
45
|
-
/* ----------------------------- fixtures ---------------------------- */
|
|
46
|
-
|
|
47
|
-
const SPACE_ID = "space-1";
|
|
48
|
-
const CONV_ID = "conv-1";
|
|
49
|
-
const MSG_ID = "msg-1";
|
|
50
|
-
|
|
51
|
-
function makeLoc(over: Partial<GenieChatLocation> = {}): GenieChatLocation {
|
|
52
|
-
return {
|
|
53
|
-
space_id: SPACE_ID,
|
|
54
|
-
conversation_id: CONV_ID,
|
|
55
|
-
message_id: MSG_ID,
|
|
56
|
-
...over,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function makeMessage(over: Partial<GenieMessage> = {}): GenieMessage {
|
|
61
|
-
return {
|
|
62
|
-
space_id: SPACE_ID,
|
|
63
|
-
conversation_id: CONV_ID,
|
|
64
|
-
message_id: MSG_ID,
|
|
65
|
-
...over,
|
|
66
|
-
} as GenieMessage;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function thought(
|
|
70
|
-
thought_type: GenieThought["thought_type"],
|
|
71
|
-
content: string,
|
|
72
|
-
): GenieThought {
|
|
73
|
-
return { thought_type, content };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Assert that `actual` contains at least the key/value pairs in
|
|
78
|
-
* `expected` (shallow, like Jest's `toMatchObject` for flat
|
|
79
|
-
* payloads).
|
|
80
|
-
*/
|
|
81
|
-
function matchObject(actual: unknown, expected: Record<string, unknown>): void {
|
|
82
|
-
assert.ok(actual && typeof actual === "object", "expected an object");
|
|
83
|
-
const a = actual as Record<string, unknown>;
|
|
84
|
-
for (const [k, v] of Object.entries(expected)) {
|
|
85
|
-
assert.deepEqual(a[k], v, `key ${k}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Recursively drop `undefined`-valued keys so a present-but-undefined
|
|
90
|
-
* optional (e.g. a detector's `title: undefined`) compares equal to an
|
|
91
|
-
* omitted one - matching the intent of the original `toEqual` tests. */
|
|
92
|
-
function pruneUndefined<T>(value: T): T {
|
|
93
|
-
if (Array.isArray(value)) return value.map(pruneUndefined) as unknown as T;
|
|
94
|
-
if (value && typeof value === "object") {
|
|
95
|
-
const out: Record<string, unknown> = {};
|
|
96
|
-
for (const [k, v] of Object.entries(value)) {
|
|
97
|
-
if (v !== undefined) out[k] = pruneUndefined(v);
|
|
98
|
-
}
|
|
99
|
-
return out as T;
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Deep-equal that ignores `undefined`-valued keys on both sides. */
|
|
105
|
-
function equalPayload(actual: unknown, expected: unknown): void {
|
|
106
|
-
assert.deepEqual(pruneUndefined(actual), pruneUndefined(expected));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* -------------------------- eventDetector -------------------------- */
|
|
110
|
-
|
|
111
|
-
describe("eventDetector", () => {
|
|
112
|
-
it("returns an EventDetector with the literal type and the detect fn", () => {
|
|
113
|
-
const fn: Parameters<typeof eventDetector<"status">>[1] = (
|
|
114
|
-
current,
|
|
115
|
-
_previous,
|
|
116
|
-
space_id,
|
|
117
|
-
) => ({
|
|
118
|
-
status: current.status!,
|
|
119
|
-
previous_status: undefined,
|
|
120
|
-
space_id,
|
|
121
|
-
conversation_id: current.conversation_id,
|
|
122
|
-
message_id: current.message_id,
|
|
123
|
-
});
|
|
124
|
-
const d = eventDetector("status", fn);
|
|
125
|
-
assert.equal(d.type, "status");
|
|
126
|
-
assert.equal(d.detect, fn);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
/* ---------------------------- detectStatus -------------------------- */
|
|
131
|
-
|
|
132
|
-
describe("detectStatus", () => {
|
|
133
|
-
it("emits with previous_status undefined on the first status seen", () => {
|
|
134
|
-
const out = detectStatus.detect(
|
|
135
|
-
makeMessage({ status: "SUBMITTED" }),
|
|
136
|
-
undefined,
|
|
137
|
-
SPACE_ID,
|
|
138
|
-
);
|
|
139
|
-
equalPayload(out, {
|
|
140
|
-
status: "SUBMITTED",
|
|
141
|
-
previous_status: undefined,
|
|
142
|
-
space_id: SPACE_ID,
|
|
143
|
-
conversation_id: CONV_ID,
|
|
144
|
-
message_id: MSG_ID,
|
|
145
|
-
} satisfies GenieChatEventFields<"status">);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("emits the transition when status differs from previous", () => {
|
|
149
|
-
const out = detectStatus.detect(
|
|
150
|
-
makeMessage({ status: "COMPLETED" }),
|
|
151
|
-
makeMessage({ status: "ASKING_AI" }),
|
|
152
|
-
SPACE_ID,
|
|
153
|
-
);
|
|
154
|
-
matchObject(out, {
|
|
155
|
-
status: "COMPLETED",
|
|
156
|
-
previous_status: "ASKING_AI",
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("does not emit when status is unchanged", () => {
|
|
161
|
-
const out = detectStatus.detect(
|
|
162
|
-
makeMessage({ status: "ASKING_AI" }),
|
|
163
|
-
makeMessage({ status: "ASKING_AI" }),
|
|
164
|
-
SPACE_ID,
|
|
165
|
-
);
|
|
166
|
-
assert.equal(out, undefined);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("does not emit when current.status is undefined", () => {
|
|
170
|
-
const out = detectStatus.detect(makeMessage(), undefined, SPACE_ID);
|
|
171
|
-
assert.equal(out, undefined);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
/* ------------------------ detectAttachmentAdded --------------------- */
|
|
176
|
-
|
|
177
|
-
describe("detectAttachmentAdded", () => {
|
|
178
|
-
it("emits on first sight (no previous) with the detected attachment_type", () => {
|
|
179
|
-
const att: GenieAttachment = { attachment_id: "a1", text: { content: "hi" } };
|
|
180
|
-
const out = detectAttachmentAdded.detect(
|
|
181
|
-
att,
|
|
182
|
-
undefined,
|
|
183
|
-
makeLoc({ attachment_id: "a1" }),
|
|
184
|
-
0,
|
|
185
|
-
);
|
|
186
|
-
equalPayload(out, {
|
|
187
|
-
...makeLoc({ attachment_id: "a1" }),
|
|
188
|
-
index: 0,
|
|
189
|
-
attachment_type: "text",
|
|
190
|
-
} satisfies GenieChatEventFields<"attachment">);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("does not emit when the slot already existed", () => {
|
|
194
|
-
const att: GenieAttachment = { attachment_id: "a1", text: { content: "hi" } };
|
|
195
|
-
assert.equal(
|
|
196
|
-
detectAttachmentAdded.detect(att, att, makeLoc({ attachment_id: "a1" }), 0),
|
|
197
|
-
undefined,
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("reports the right attachment_type for query / suggested_questions attachments", () => {
|
|
202
|
-
matchObject(
|
|
203
|
-
detectAttachmentAdded.detect(
|
|
204
|
-
{ attachment_id: "q1", query: { query: "SELECT 1" } },
|
|
205
|
-
undefined,
|
|
206
|
-
makeLoc({ attachment_id: "q1" }),
|
|
207
|
-
1,
|
|
208
|
-
),
|
|
209
|
-
{ index: 1, attachment_type: "query" },
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
matchObject(
|
|
213
|
-
detectAttachmentAdded.detect(
|
|
214
|
-
{
|
|
215
|
-
attachment_id: "sq1",
|
|
216
|
-
suggested_questions: { questions: ["Foo?", "Bar?"] },
|
|
217
|
-
},
|
|
218
|
-
undefined,
|
|
219
|
-
makeLoc({ attachment_id: "sq1" }),
|
|
220
|
-
2,
|
|
221
|
-
),
|
|
222
|
-
{ index: 2, attachment_type: "suggested_questions" },
|
|
223
|
-
);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
/* ---------------------------- detectThinking ------------------------ */
|
|
228
|
-
|
|
229
|
-
describe("detectThinking", () => {
|
|
230
|
-
it("returns undefined when the attachment has no thoughts", () => {
|
|
231
|
-
const att: GenieAttachment = { attachment_id: "q1", query: { query: "SELECT 1" } };
|
|
232
|
-
assert.equal(detectThinking.detect(att, undefined, makeLoc(), 0), undefined);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("emits one event per thought on the first observation", () => {
|
|
236
|
-
const att: GenieAttachment = {
|
|
237
|
-
attachment_id: "q1",
|
|
238
|
-
query: {
|
|
239
|
-
thoughts: [
|
|
240
|
-
thought("THOUGHT_TYPE_DESCRIPTION", "what the user asked"),
|
|
241
|
-
thought("THOUGHT_TYPE_STEPS", "step 1"),
|
|
242
|
-
],
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
const out = detectThinking.detect(att, undefined, makeLoc(), 0);
|
|
246
|
-
equalPayload(out, [
|
|
247
|
-
{
|
|
248
|
-
...makeLoc(),
|
|
249
|
-
text: "what the user asked",
|
|
250
|
-
thought_type: "THOUGHT_TYPE_DESCRIPTION",
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
...makeLoc(),
|
|
254
|
-
text: "step 1",
|
|
255
|
-
thought_type: "THOUGHT_TYPE_STEPS",
|
|
256
|
-
},
|
|
257
|
-
] satisfies GenieChatEventFields<"thinking">[]);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("emits only the newly-added (type, content) tuples on a subsequent snapshot", () => {
|
|
261
|
-
const prev: GenieAttachment = {
|
|
262
|
-
attachment_id: "q1",
|
|
263
|
-
query: {
|
|
264
|
-
thoughts: [thought("THOUGHT_TYPE_DESCRIPTION", "first")],
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
const curr: GenieAttachment = {
|
|
268
|
-
attachment_id: "q1",
|
|
269
|
-
query: {
|
|
270
|
-
thoughts: [
|
|
271
|
-
thought("THOUGHT_TYPE_DESCRIPTION", "first"),
|
|
272
|
-
thought("THOUGHT_TYPE_STEPS", "second"),
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
const out = detectThinking.detect(curr, prev, makeLoc(), 0);
|
|
277
|
-
equalPayload(out, [
|
|
278
|
-
{ ...makeLoc(), text: "second", thought_type: "THOUGHT_TYPE_STEPS" },
|
|
279
|
-
]);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("uses a value-based set diff so re-typed / reordered thoughts only emit the new tuple", () => {
|
|
283
|
-
// Genie can mutate index 0 in place (e.g. promote a DATA_SOURCING
|
|
284
|
-
// thought to DESCRIPTION while re-appending the original at
|
|
285
|
-
// index 1). A positional diff would mis-report the re-typed
|
|
286
|
-
// slot as new and re-emit the moved one.
|
|
287
|
-
const prev: GenieAttachment = {
|
|
288
|
-
attachment_id: "q1",
|
|
289
|
-
query: {
|
|
290
|
-
thoughts: [thought("THOUGHT_TYPE_DATA_SOURCING", "tables...")],
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
const curr: GenieAttachment = {
|
|
294
|
-
attachment_id: "q1",
|
|
295
|
-
query: {
|
|
296
|
-
thoughts: [
|
|
297
|
-
thought("THOUGHT_TYPE_DESCRIPTION", "restatement"),
|
|
298
|
-
thought("THOUGHT_TYPE_DATA_SOURCING", "tables..."),
|
|
299
|
-
],
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
const out = detectThinking.detect(curr, prev, makeLoc(), 0);
|
|
303
|
-
equalPayload(out, [
|
|
304
|
-
{
|
|
305
|
-
...makeLoc(),
|
|
306
|
-
text: "restatement",
|
|
307
|
-
thought_type: "THOUGHT_TYPE_DESCRIPTION",
|
|
308
|
-
},
|
|
309
|
-
]);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("dedupes within a single snapshot if Genie ever ships the same tuple twice", () => {
|
|
313
|
-
const att: GenieAttachment = {
|
|
314
|
-
attachment_id: "q1",
|
|
315
|
-
query: {
|
|
316
|
-
thoughts: [
|
|
317
|
-
thought("THOUGHT_TYPE_STEPS", "step"),
|
|
318
|
-
thought("THOUGHT_TYPE_STEPS", "step"),
|
|
319
|
-
],
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
const out = detectThinking.detect(att, undefined, makeLoc(), 0);
|
|
323
|
-
equalPayload(out, [
|
|
324
|
-
{ ...makeLoc(), text: "step", thought_type: "THOUGHT_TYPE_STEPS" },
|
|
325
|
-
]);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
/* ----------------------------- detectText --------------------------- */
|
|
330
|
-
|
|
331
|
-
describe("detectText", () => {
|
|
332
|
-
it("emits when text content first appears", () => {
|
|
333
|
-
const out = detectText.detect(
|
|
334
|
-
{ attachment_id: "t1", text: { content: "hello" } },
|
|
335
|
-
undefined,
|
|
336
|
-
makeLoc({ attachment_id: "t1" }),
|
|
337
|
-
0,
|
|
338
|
-
);
|
|
339
|
-
equalPayload(out, { ...makeLoc({ attachment_id: "t1" }), text: "hello" });
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("emits when text content changes", () => {
|
|
343
|
-
const out = detectText.detect(
|
|
344
|
-
{ attachment_id: "t1", text: { content: "hello world" } },
|
|
345
|
-
{ attachment_id: "t1", text: { content: "hello" } },
|
|
346
|
-
makeLoc({ attachment_id: "t1" }),
|
|
347
|
-
0,
|
|
348
|
-
);
|
|
349
|
-
matchObject(out, { text: "hello world" });
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it("does not emit when content is unchanged", () => {
|
|
353
|
-
const same: GenieAttachment = { attachment_id: "t1", text: { content: "x" } };
|
|
354
|
-
assert.equal(detectText.detect(same, same, makeLoc(), 0), undefined);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("does not emit when text is undefined", () => {
|
|
358
|
-
assert.equal(
|
|
359
|
-
detectText.detect({ attachment_id: "x" }, undefined, makeLoc(), 0),
|
|
360
|
-
undefined,
|
|
361
|
-
);
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
/* ----------------------------- detectQuery -------------------------- */
|
|
366
|
-
|
|
367
|
-
describe("detectQuery", () => {
|
|
368
|
-
it("emits when SQL first appears", () => {
|
|
369
|
-
const out = detectQuery.detect(
|
|
370
|
-
{ attachment_id: "q1", query: { query: "SELECT 1" } },
|
|
371
|
-
{ attachment_id: "q1", query: {} },
|
|
372
|
-
makeLoc({ attachment_id: "q1" }),
|
|
373
|
-
0,
|
|
374
|
-
);
|
|
375
|
-
equalPayload(out, { ...makeLoc({ attachment_id: "q1" }), sql: "SELECT 1" });
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("emits when SQL is rewritten", () => {
|
|
379
|
-
const out = detectQuery.detect(
|
|
380
|
-
{ attachment_id: "q1", query: { query: "SELECT 2" } },
|
|
381
|
-
{ attachment_id: "q1", query: { query: "SELECT 1" } },
|
|
382
|
-
makeLoc({ attachment_id: "q1" }),
|
|
383
|
-
0,
|
|
384
|
-
);
|
|
385
|
-
matchObject(out, { sql: "SELECT 2" });
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("does not emit when SQL is unchanged or missing", () => {
|
|
389
|
-
const same: GenieAttachment = {
|
|
390
|
-
attachment_id: "q1",
|
|
391
|
-
query: { query: "SELECT 1" },
|
|
392
|
-
};
|
|
393
|
-
assert.equal(detectQuery.detect(same, same, makeLoc(), 0), undefined);
|
|
394
|
-
assert.equal(
|
|
395
|
-
detectQuery.detect({ attachment_id: "q1", query: {} }, undefined, makeLoc(), 0),
|
|
396
|
-
undefined,
|
|
397
|
-
);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
/* ---------------------------- detectStatement ----------------------- */
|
|
402
|
-
|
|
403
|
-
describe("detectStatement", () => {
|
|
404
|
-
it("emits when statement_id transitions undefined -> string", () => {
|
|
405
|
-
const out = detectStatement.detect(
|
|
406
|
-
{ attachment_id: "q1", query: { statement_id: "stmt-1" } },
|
|
407
|
-
{ attachment_id: "q1", query: {} },
|
|
408
|
-
makeLoc({ attachment_id: "q1" }),
|
|
409
|
-
0,
|
|
410
|
-
);
|
|
411
|
-
equalPayload(out, {
|
|
412
|
-
...makeLoc({ attachment_id: "q1" }),
|
|
413
|
-
statement_id: "stmt-1",
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("does not emit when statement_id is unchanged", () => {
|
|
418
|
-
const a: GenieAttachment = {
|
|
419
|
-
attachment_id: "q1",
|
|
420
|
-
query: { statement_id: "stmt-1" },
|
|
421
|
-
};
|
|
422
|
-
assert.equal(detectStatement.detect(a, a, makeLoc(), 0), undefined);
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
/* ----------------------------- detectRows --------------------------- */
|
|
427
|
-
|
|
428
|
-
describe("detectRows", () => {
|
|
429
|
-
it("emits on undefined -> 0 (initial observation)", () => {
|
|
430
|
-
const out = detectRows.detect(
|
|
431
|
-
{
|
|
432
|
-
attachment_id: "q1",
|
|
433
|
-
query: { query_result_metadata: { row_count: 0 } },
|
|
434
|
-
},
|
|
435
|
-
{ attachment_id: "q1", query: {} },
|
|
436
|
-
makeLoc({ attachment_id: "q1" }),
|
|
437
|
-
0,
|
|
438
|
-
);
|
|
439
|
-
matchObject(out, { row_count: 0, previous_row_count: undefined });
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
it("emits on 0 -> N once warehouse execution completes", () => {
|
|
443
|
-
const out = detectRows.detect(
|
|
444
|
-
{
|
|
445
|
-
attachment_id: "q1",
|
|
446
|
-
query: {
|
|
447
|
-
statement_id: "stmt-1",
|
|
448
|
-
query_result_metadata: { row_count: 42 },
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
attachment_id: "q1",
|
|
453
|
-
query: {
|
|
454
|
-
statement_id: "stmt-1",
|
|
455
|
-
query_result_metadata: { row_count: 0 },
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
makeLoc({ attachment_id: "q1" }),
|
|
459
|
-
0,
|
|
460
|
-
);
|
|
461
|
-
matchObject(out, {
|
|
462
|
-
row_count: 42,
|
|
463
|
-
previous_row_count: 0,
|
|
464
|
-
statement_id: "stmt-1",
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("does not emit when row_count is unchanged", () => {
|
|
469
|
-
const a: GenieAttachment = {
|
|
470
|
-
attachment_id: "q1",
|
|
471
|
-
query: { query_result_metadata: { row_count: 5 } },
|
|
472
|
-
};
|
|
473
|
-
assert.equal(detectRows.detect(a, a, makeLoc(), 0), undefined);
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
/* ----------------------- detectSuggestedQuestions ------------------- */
|
|
478
|
-
|
|
479
|
-
describe("detectSuggestedQuestions", () => {
|
|
480
|
-
it("emits when questions first appear", () => {
|
|
481
|
-
const out = detectSuggestedQuestions.detect(
|
|
482
|
-
{
|
|
483
|
-
attachment_id: "sq1",
|
|
484
|
-
suggested_questions: { questions: ["Foo?", "Bar?"] },
|
|
485
|
-
},
|
|
486
|
-
undefined,
|
|
487
|
-
makeLoc({ attachment_id: "sq1" }),
|
|
488
|
-
0,
|
|
489
|
-
);
|
|
490
|
-
matchObject(out, { questions: ["Foo?", "Bar?"] });
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it("emits when the questions list is rewritten (length-preserving)", () => {
|
|
494
|
-
const out = detectSuggestedQuestions.detect(
|
|
495
|
-
{
|
|
496
|
-
attachment_id: "sq1",
|
|
497
|
-
suggested_questions: { questions: ["A?", "B?"] },
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
attachment_id: "sq1",
|
|
501
|
-
suggested_questions: { questions: ["A?", "C?"] },
|
|
502
|
-
},
|
|
503
|
-
makeLoc({ attachment_id: "sq1" }),
|
|
504
|
-
0,
|
|
505
|
-
);
|
|
506
|
-
matchObject(out, { questions: ["A?", "B?"] });
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it("does not emit on an empty list", () => {
|
|
510
|
-
assert.equal(
|
|
511
|
-
detectSuggestedQuestions.detect(
|
|
512
|
-
{ attachment_id: "sq1", suggested_questions: { questions: [] } },
|
|
513
|
-
undefined,
|
|
514
|
-
makeLoc(),
|
|
515
|
-
0,
|
|
516
|
-
),
|
|
517
|
-
undefined,
|
|
518
|
-
);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it("does not emit when the JSON-stringified list is unchanged", () => {
|
|
522
|
-
const same: GenieAttachment = {
|
|
523
|
-
attachment_id: "sq1",
|
|
524
|
-
suggested_questions: { questions: ["Foo?", "Bar?"] },
|
|
525
|
-
};
|
|
526
|
-
assert.equal(detectSuggestedQuestions.detect(same, same, makeLoc(), 0), undefined);
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
/* -------------------------- eventsFromMessage ----------------------- */
|
|
531
|
-
|
|
532
|
-
describe("eventsFromMessage", () => {
|
|
533
|
-
// Drain the sync generator to an array. Every yield is a flat
|
|
534
|
-
// `{type, ...fields}` object per the GenieChatEvent contract;
|
|
535
|
-
// the discriminator narrows the rest of the fields per variant.
|
|
536
|
-
function collect(
|
|
537
|
-
current: GenieMessage,
|
|
538
|
-
previous: GenieMessage | undefined,
|
|
539
|
-
space_id: string = SPACE_ID,
|
|
540
|
-
): GenieChatEvent[] {
|
|
541
|
-
return [...eventsFromMessage(current, previous, space_id)];
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
it("yields every event with the type discriminator stamped", () => {
|
|
545
|
-
const curr = makeMessage({
|
|
546
|
-
status: "ASKING_AI",
|
|
547
|
-
attachments: [{ attachment_id: "q1", query: { query: "SELECT 1" } }],
|
|
548
|
-
});
|
|
549
|
-
const events = collect(curr, undefined);
|
|
550
|
-
// Each variant carries its discriminator.
|
|
551
|
-
for (const e of events) {
|
|
552
|
-
assert.equal(typeof e.type, "string");
|
|
553
|
-
}
|
|
554
|
-
assert.equal(events[0]?.type, "status");
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
it("dispatches status before any attachment events", () => {
|
|
558
|
-
const curr = makeMessage({
|
|
559
|
-
status: "ASKING_AI",
|
|
560
|
-
attachments: [{ attachment_id: "q1", query: { query: "SELECT 1" } }],
|
|
561
|
-
});
|
|
562
|
-
const events = collect(curr, undefined);
|
|
563
|
-
assert.deepEqual(
|
|
564
|
-
events.map((e) => e.type),
|
|
565
|
-
["status", "attachment", "query"],
|
|
566
|
-
);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it("fans out per-attachment events with the correct index", () => {
|
|
570
|
-
const curr = makeMessage({
|
|
571
|
-
attachments: [
|
|
572
|
-
{ attachment_id: "t1", text: { content: "hi" } },
|
|
573
|
-
{ attachment_id: "q1", query: { query: "SELECT 1" } },
|
|
574
|
-
],
|
|
575
|
-
});
|
|
576
|
-
const events = collect(curr, undefined);
|
|
577
|
-
|
|
578
|
-
const attachmentEvents = events.filter(
|
|
579
|
-
(e): e is AttachmentEvent => e.type === "attachment",
|
|
580
|
-
);
|
|
581
|
-
assert.equal(attachmentEvents.length, 2);
|
|
582
|
-
matchObject(attachmentEvents[0], {
|
|
583
|
-
index: 0,
|
|
584
|
-
attachment_type: "text",
|
|
585
|
-
attachment_id: "t1",
|
|
586
|
-
});
|
|
587
|
-
matchObject(attachmentEvents[1], {
|
|
588
|
-
index: 1,
|
|
589
|
-
attachment_type: "query",
|
|
590
|
-
attachment_id: "q1",
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it("matches an id'd attachment to the prev slot by id regardless of position", () => {
|
|
595
|
-
const prev = makeMessage({
|
|
596
|
-
attachments: [
|
|
597
|
-
{ attachment_id: "a", text: { content: "x" } },
|
|
598
|
-
{ attachment_id: "b", query: { query: "SELECT 1" } },
|
|
599
|
-
],
|
|
600
|
-
});
|
|
601
|
-
// Same attachments, swapped order. The query SQL is unchanged
|
|
602
|
-
// and the text attachment is unchanged, so no `query` /
|
|
603
|
-
// `text` events should fire even though positional matching
|
|
604
|
-
// would think both are brand-new.
|
|
605
|
-
const curr = makeMessage({
|
|
606
|
-
attachments: [
|
|
607
|
-
{ attachment_id: "b", query: { query: "SELECT 1" } },
|
|
608
|
-
{ attachment_id: "a", text: { content: "x" } },
|
|
609
|
-
],
|
|
610
|
-
});
|
|
611
|
-
const events = collect(curr, prev);
|
|
612
|
-
assert.equal(events.filter((e) => e.type === "attachment").length, 0);
|
|
613
|
-
assert.equal(events.filter((e) => e.type === "query").length, 0);
|
|
614
|
-
assert.equal(events.filter((e) => e.type === "text").length, 0);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it("matches anonymous attachments positionally and does not bind to an id'd predecessor", () => {
|
|
618
|
-
const prev = makeMessage({
|
|
619
|
-
attachments: [{ text: { content: "old" } }],
|
|
620
|
-
});
|
|
621
|
-
const curr = makeMessage({
|
|
622
|
-
attachments: [{ text: { content: "new" } }],
|
|
623
|
-
});
|
|
624
|
-
const events = collect(curr, prev);
|
|
625
|
-
|
|
626
|
-
const textEvents = events.filter((e) => e.type === "text");
|
|
627
|
-
assert.equal(textEvents.length, 1);
|
|
628
|
-
matchObject(textEvents[0], { text: "new" });
|
|
629
|
-
assert.equal(events.filter((e) => e.type === "attachment").length, 0);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it("does not bind an id'd attachment to an anonymous predecessor at the same slot", () => {
|
|
633
|
-
const prev = makeMessage({
|
|
634
|
-
attachments: [{ text: { content: "x" } }],
|
|
635
|
-
});
|
|
636
|
-
const curr = makeMessage({
|
|
637
|
-
attachments: [{ attachment_id: "a", text: { content: "x" } }],
|
|
638
|
-
});
|
|
639
|
-
const events = collect(curr, prev);
|
|
640
|
-
const attachmentEvents = events.filter(
|
|
641
|
-
(e): e is AttachmentEvent => e.type === "attachment",
|
|
642
|
-
);
|
|
643
|
-
assert.equal(attachmentEvents.length, 1);
|
|
644
|
-
matchObject(attachmentEvents[0], {
|
|
645
|
-
attachment_id: "a",
|
|
646
|
-
attachment_type: "text",
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
it("does NOT emit message or result (those are lifecycle, handled by the chat driver)", () => {
|
|
651
|
-
// Even when the snapshot's status is terminal,
|
|
652
|
-
// `eventsFromMessage` is pure-diff and shouldn't emit the
|
|
653
|
-
// lifecycle envelope.
|
|
654
|
-
const curr = makeMessage({ status: "COMPLETED" });
|
|
655
|
-
const events = collect(curr, undefined);
|
|
656
|
-
assert.equal(
|
|
657
|
-
events.some((e) => e.type === "message"),
|
|
658
|
-
false,
|
|
659
|
-
);
|
|
660
|
-
assert.equal(
|
|
661
|
-
events.some((e) => e.type === "result"),
|
|
662
|
-
false,
|
|
663
|
-
);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it("no-ops on attachments[] when nothing changed", () => {
|
|
667
|
-
const a = makeMessage({
|
|
668
|
-
status: "ASKING_AI",
|
|
669
|
-
attachments: [{ attachment_id: "q1", query: { query: "SELECT 1" } }],
|
|
670
|
-
});
|
|
671
|
-
const events = collect(a, a);
|
|
672
|
-
assert.equal(events.length, 0);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
it("yields multiple thinking events as separate flat events", () => {
|
|
676
|
-
const curr = makeMessage({
|
|
677
|
-
attachments: [
|
|
678
|
-
{
|
|
679
|
-
attachment_id: "q1",
|
|
680
|
-
query: {
|
|
681
|
-
thoughts: [
|
|
682
|
-
thought("THOUGHT_TYPE_DESCRIPTION", "first"),
|
|
683
|
-
thought("THOUGHT_TYPE_STEPS", "second"),
|
|
684
|
-
],
|
|
685
|
-
},
|
|
686
|
-
},
|
|
687
|
-
],
|
|
688
|
-
});
|
|
689
|
-
const events = collect(curr, undefined);
|
|
690
|
-
const thinking = events.filter((e): e is ThinkingEvent => e.type === "thinking");
|
|
691
|
-
assert.equal(thinking.length, 2);
|
|
692
|
-
assert.equal(thinking[0]!.thought_type, "THOUGHT_TYPE_DESCRIPTION");
|
|
693
|
-
assert.equal(thinking[1]!.thought_type, "THOUGHT_TYPE_STEPS");
|
|
694
|
-
});
|
|
695
|
-
});
|
package/test/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
-
{
|
|
3
|
-
"extends": "../tsconfig.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"noEmit": true,
|
|
6
|
-
"rootDir": ".."
|
|
7
|
-
},
|
|
8
|
-
"include": [
|
|
9
|
-
"**/*.ts"
|
|
10
|
-
],
|
|
11
|
-
"exclude": [
|
|
12
|
-
"node_modules"
|
|
13
|
-
]
|
|
14
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
-
{
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
|
-
"outDir": "lib",
|
|
6
|
-
"alwaysStrict": true,
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"experimentalDecorators": true,
|
|
10
|
-
"inlineSourceMap": true,
|
|
11
|
-
"inlineSources": true,
|
|
12
|
-
"lib": [
|
|
13
|
-
"ES2022",
|
|
14
|
-
"WebWorker"
|
|
15
|
-
],
|
|
16
|
-
"module": "ESNext",
|
|
17
|
-
"noEmitOnError": false,
|
|
18
|
-
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"noImplicitAny": true,
|
|
20
|
-
"noImplicitReturns": true,
|
|
21
|
-
"noImplicitThis": true,
|
|
22
|
-
"noUnusedLocals": true,
|
|
23
|
-
"noUnusedParameters": true,
|
|
24
|
-
"resolveJsonModule": true,
|
|
25
|
-
"strict": true,
|
|
26
|
-
"strictNullChecks": true,
|
|
27
|
-
"strictPropertyInitialization": true,
|
|
28
|
-
"stripInternal": true,
|
|
29
|
-
"target": "ES2022",
|
|
30
|
-
"types": [],
|
|
31
|
-
"moduleResolution": "bundler",
|
|
32
|
-
"skipLibCheck": true
|
|
33
|
-
},
|
|
34
|
-
"include": [
|
|
35
|
-
"src/**/*.ts"
|
|
36
|
-
],
|
|
37
|
-
"exclude": [
|
|
38
|
-
"node_modules"
|
|
39
|
-
]
|
|
40
|
-
}
|