@specific.dev/spectest 0.106.0 → 0.107.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.
- package/README.md +21 -0
- package/dist/annotate.d.ts +15 -0
- package/dist/annotate.js +10 -0
- package/dist/recorder.d.ts +9 -0
- package/package.json +1 -1
- package/src/annotate.test.ts +67 -0
- package/src/annotate.ts +24 -0
- package/src/recorder.ts +9 -0
package/README.md
CHANGED
|
@@ -107,6 +107,27 @@ return annotate(turns, "chat", {
|
|
|
107
107
|
});
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
## Inline replies (0.107.0)
|
|
111
|
+
|
|
112
|
+
A `chat` message can carry an `id` — any non-empty string the fake chooses,
|
|
113
|
+
the provider's message handle usually; it is never drawn — and a reply names
|
|
114
|
+
an earlier message's `id` as `replyTo`. The dashboard draws the reply the
|
|
115
|
+
way a messaging app threads one: an excerpt of the earlier message in a
|
|
116
|
+
muted mini-bubble directly above the reply, on the reply's side, with a
|
|
117
|
+
short connector between them. A `replyTo` that names no earlier message of
|
|
118
|
+
the same call draws nothing extra; the bubble is an ordinary message.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
return annotate(turns, "chat", {
|
|
122
|
+
messages: turns.map((t) => ({
|
|
123
|
+
id: t.id,
|
|
124
|
+
side: t.fromCustomer ? "self" : "other",
|
|
125
|
+
text: t.body,
|
|
126
|
+
replyTo: t.inReplyTo,
|
|
127
|
+
})),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
110
131
|
## HTTP assertions
|
|
111
132
|
|
|
112
133
|
`ctx.fetch()` returns a wrapped response. Status fields, header lookups, and
|
package/dist/annotate.d.ts
CHANGED
|
@@ -140,6 +140,21 @@ export interface ChatMessageAnnotation {
|
|
|
140
140
|
* qualify and is dropped.
|
|
141
141
|
*/
|
|
142
142
|
statusAt?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The author's handle for this message — the provider's message id,
|
|
145
|
+
* usually — so a later message can reply to it by name. Any non-empty
|
|
146
|
+
* string; the dashboard never draws it.
|
|
147
|
+
*/
|
|
148
|
+
id?: string;
|
|
149
|
+
/**
|
|
150
|
+
* The `id` of an earlier message in this transcript that this one
|
|
151
|
+
* replies to, drawn the way a messaging app threads a reply: an excerpt
|
|
152
|
+
* of that message quoted above this bubble, on this bubble's side. One
|
|
153
|
+
* that names no earlier message of the same call — an unknown id, a
|
|
154
|
+
* later message, the message itself — draws nothing extra, and the
|
|
155
|
+
* bubble reads as an ordinary message.
|
|
156
|
+
*/
|
|
157
|
+
replyTo?: string;
|
|
143
158
|
}
|
|
144
159
|
/** One reaction on a {@link ChatMessageAnnotation}. */
|
|
145
160
|
export interface ChatReactionAnnotation {
|
package/dist/annotate.js
CHANGED
|
@@ -191,6 +191,16 @@ function chatMessageOf(src) {
|
|
|
191
191
|
msg.statusAt = truncateUtf8(src.statusAt).value;
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
// A reply threads to an earlier message by the handle its author gave
|
|
195
|
+
// it, so both ride through as given — a non-empty string, cut like a
|
|
196
|
+
// body — and anything else is dropped. Whether `replyTo` names an
|
|
197
|
+
// earlier message of the transcript is the dashboard's to decide: it
|
|
198
|
+
// draws the quote when it can and an ordinary bubble when it cannot.
|
|
199
|
+
if (typeof src.id === "string" && src.id !== "")
|
|
200
|
+
msg.id = truncateUtf8(src.id).value;
|
|
201
|
+
if (typeof src.replyTo === "string" && src.replyTo !== "") {
|
|
202
|
+
msg.replyTo = truncateUtf8(src.replyTo).value;
|
|
203
|
+
}
|
|
194
204
|
return msg;
|
|
195
205
|
}
|
|
196
206
|
/** The block's `typing`: `self` or `other`, else nobody. The thread's
|
package/dist/recorder.d.ts
CHANGED
|
@@ -660,6 +660,15 @@ export interface ChatBlockMessage {
|
|
|
660
660
|
* text drawn after the status caption ("Read 23:14"). Rides with
|
|
661
661
|
* `status`; alone it has nothing to qualify. */
|
|
662
662
|
statusAt?: string;
|
|
663
|
+
/** The author's handle for this message — the provider's message id,
|
|
664
|
+
* usually — which a later message's `replyTo` names. Never drawn.
|
|
665
|
+
* Absent when the author gave none. */
|
|
666
|
+
id?: string;
|
|
667
|
+
/** The `id` of an earlier message in the same block this one replies
|
|
668
|
+
* to, drawn as a quoted excerpt of that message above the bubble, the
|
|
669
|
+
* way a messaging app threads a reply. One that names no earlier
|
|
670
|
+
* message draws nothing extra. */
|
|
671
|
+
replyTo?: string;
|
|
663
672
|
}
|
|
664
673
|
/** One reaction on a {@link ChatBlockMessage}. */
|
|
665
674
|
export interface ChatBlockReaction {
|
package/package.json
CHANGED
package/src/annotate.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { annotate, readAnnotation, type ChatMessageAnnotation } from "./annotate.js";
|
|
4
|
+
import { truncateUtf8 } from "./recorder.js";
|
|
4
5
|
|
|
5
6
|
/** Open a chat annotation: the messages its one block lowered to, and the
|
|
6
7
|
* value the test would receive. */
|
|
@@ -135,3 +136,69 @@ describe("annotate chat status and typing", () => {
|
|
|
135
136
|
expect(chat(annotate(messages, "chat")).typing).toBeUndefined();
|
|
136
137
|
});
|
|
137
138
|
});
|
|
139
|
+
|
|
140
|
+
describe("annotate chat inline replies", () => {
|
|
141
|
+
test("threads a reply to the earlier message it names and leaves the rest alone", () => {
|
|
142
|
+
const { messages } = chat(
|
|
143
|
+
annotate(
|
|
144
|
+
[
|
|
145
|
+
{ id: "m1", side: "other", text: "Let me check." },
|
|
146
|
+
{ id: "m2", side: "self", text: "where is my order?", replyTo: "m1", new: true },
|
|
147
|
+
{ side: "other", text: "It ships today." },
|
|
148
|
+
],
|
|
149
|
+
"chat",
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
expect(messages[0]).toEqual({ id: "m1", side: "other", text: "Let me check." });
|
|
153
|
+
expect(messages[1]).toEqual({ id: "m2", side: "self", text: "where is my order?", replyTo: "m1", new: true });
|
|
154
|
+
// A message with neither carries neither key.
|
|
155
|
+
expect(messages[2]).toEqual({ side: "other", text: "It ships today." });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Both are the author's handles, carried as given: the dashboard sees
|
|
159
|
+
// the whole transcript and decides whether a `replyTo` names an earlier
|
|
160
|
+
// message, drawing an ordinary bubble when it does not. Only a value
|
|
161
|
+
// that is not a non-empty string is dropped.
|
|
162
|
+
test("drops an id or a replyTo that is not a non-empty string and carries the rest as given", () => {
|
|
163
|
+
const { messages } = chat(
|
|
164
|
+
annotate(
|
|
165
|
+
[
|
|
166
|
+
{ id: 42, side: "self", text: "a", replyTo: 7 },
|
|
167
|
+
{ id: "", side: "self", text: "b", replyTo: "" },
|
|
168
|
+
{ id: null, side: "self", text: "c", replyTo: true },
|
|
169
|
+
{ id: "m4", side: "self", text: "d", replyTo: "nowhere" },
|
|
170
|
+
],
|
|
171
|
+
"chat",
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
expect(messages[0]).toEqual({ side: "self", text: "a" });
|
|
175
|
+
expect(messages[1]).toEqual({ side: "self", text: "b" });
|
|
176
|
+
expect(messages[2]).toEqual({ side: "self", text: "c" });
|
|
177
|
+
expect(messages[3]).toEqual({ id: "m4", side: "self", text: "d", replyTo: "nowhere" });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("an id is cut like a body, and the replyTo that names it the same way", () => {
|
|
181
|
+
// Past the recorder's snippet cap, so the cut is visible.
|
|
182
|
+
const long = "m".repeat(300_000);
|
|
183
|
+
const { messages } = chat(
|
|
184
|
+
annotate(
|
|
185
|
+
[
|
|
186
|
+
{ id: long, side: "other", text: "Let me check." },
|
|
187
|
+
{ side: "self", text: "ok", replyTo: long },
|
|
188
|
+
],
|
|
189
|
+
"chat",
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
expect(messages[0]!.id).toBe(truncateUtf8(long).value);
|
|
193
|
+
expect(messages[0]!.id!.length).toBeLessThan(long.length);
|
|
194
|
+
expect(messages[1]!.replyTo).toBe(messages[0]!.id);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("replies ride along when the messages are passed as options", () => {
|
|
198
|
+
const messages: ChatMessageAnnotation[] = [
|
|
199
|
+
{ id: "m1", side: "other", text: "Done?" },
|
|
200
|
+
{ id: "m2", side: "self", text: "Yes.", replyTo: "m1" },
|
|
201
|
+
];
|
|
202
|
+
expect(chat(annotate({ id: 7 }, "chat", { title: "+46 70 123 45 67", messages })).messages).toEqual(messages);
|
|
203
|
+
});
|
|
204
|
+
});
|
package/src/annotate.ts
CHANGED
|
@@ -199,6 +199,21 @@ export interface ChatMessageAnnotation {
|
|
|
199
199
|
* qualify and is dropped.
|
|
200
200
|
*/
|
|
201
201
|
statusAt?: string;
|
|
202
|
+
/**
|
|
203
|
+
* The author's handle for this message — the provider's message id,
|
|
204
|
+
* usually — so a later message can reply to it by name. Any non-empty
|
|
205
|
+
* string; the dashboard never draws it.
|
|
206
|
+
*/
|
|
207
|
+
id?: string;
|
|
208
|
+
/**
|
|
209
|
+
* The `id` of an earlier message in this transcript that this one
|
|
210
|
+
* replies to, drawn the way a messaging app threads a reply: an excerpt
|
|
211
|
+
* of that message quoted above this bubble, on this bubble's side. One
|
|
212
|
+
* that names no earlier message of the same call — an unknown id, a
|
|
213
|
+
* later message, the message itself — draws nothing extra, and the
|
|
214
|
+
* bubble reads as an ordinary message.
|
|
215
|
+
*/
|
|
216
|
+
replyTo?: string;
|
|
202
217
|
}
|
|
203
218
|
|
|
204
219
|
/** One reaction on a {@link ChatMessageAnnotation}. */
|
|
@@ -394,6 +409,15 @@ function chatMessageOf(src: Record<string, unknown>): ChatBlockMessage {
|
|
|
394
409
|
msg.statusAt = truncateUtf8(src.statusAt).value;
|
|
395
410
|
}
|
|
396
411
|
}
|
|
412
|
+
// A reply threads to an earlier message by the handle its author gave
|
|
413
|
+
// it, so both ride through as given — a non-empty string, cut like a
|
|
414
|
+
// body — and anything else is dropped. Whether `replyTo` names an
|
|
415
|
+
// earlier message of the transcript is the dashboard's to decide: it
|
|
416
|
+
// draws the quote when it can and an ordinary bubble when it cannot.
|
|
417
|
+
if (typeof src.id === "string" && src.id !== "") msg.id = truncateUtf8(src.id).value;
|
|
418
|
+
if (typeof src.replyTo === "string" && src.replyTo !== "") {
|
|
419
|
+
msg.replyTo = truncateUtf8(src.replyTo).value;
|
|
420
|
+
}
|
|
397
421
|
return msg;
|
|
398
422
|
}
|
|
399
423
|
|
package/src/recorder.ts
CHANGED
|
@@ -912,6 +912,15 @@ export interface ChatBlockMessage {
|
|
|
912
912
|
* text drawn after the status caption ("Read 23:14"). Rides with
|
|
913
913
|
* `status`; alone it has nothing to qualify. */
|
|
914
914
|
statusAt?: string;
|
|
915
|
+
/** The author's handle for this message — the provider's message id,
|
|
916
|
+
* usually — which a later message's `replyTo` names. Never drawn.
|
|
917
|
+
* Absent when the author gave none. */
|
|
918
|
+
id?: string;
|
|
919
|
+
/** The `id` of an earlier message in the same block this one replies
|
|
920
|
+
* to, drawn as a quoted excerpt of that message above the bubble, the
|
|
921
|
+
* way a messaging app threads a reply. One that names no earlier
|
|
922
|
+
* message draws nothing extra. */
|
|
923
|
+
replyTo?: string;
|
|
915
924
|
}
|
|
916
925
|
|
|
917
926
|
/** One reaction on a {@link ChatBlockMessage}. */
|