talon-agent 1.9.0 → 1.9.2

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.
@@ -1,296 +0,0 @@
1
- /**
2
- * Extended prompt-builder tests — covers branches and edge cases not exercised
3
- * by the existing prompt-builder.test.ts.
4
- *
5
- * enrichDMPrompt edge cases:
6
- * - Empty-string username (falsy → no @tag appended)
7
- * - Whitespace-only name
8
- * - Multi-line prompt body preserved exactly
9
- * - Special characters in name / username
10
- *
11
- * enrichGroupPrompt branches:
12
- * - Exactly 2 messages (boundary: 1 prior — triggers the formatting path)
13
- * - 6 messages (5 prior + 1 current — tests full-width context window)
14
- * - Line 33 defensive branch: priorMsgs.length === 0 cannot be reached via
15
- * recentMsgs.length > 1 guard, but the behaviour is confirmed by the
16
- * single-message case already in base tests; we verify the slice is correct.
17
- * - senderName is taken from priorMsgs[0], not the current message
18
- * - Timestamp formatting via formatSmartTimestamp is invoked (output contains
19
- * a formatted timestamp string within the context block)
20
- * - Text truncation at exactly 200 characters (boundary check)
21
- * - Text shorter than 200 characters is not padded
22
- * - getRecentBySenderId is called with the correct chatId and senderId
23
- * - Output structure: context block precedes the prompt, separated by blank line
24
- */
25
-
26
- import { describe, it, expect, vi, beforeEach } from "vitest";
27
-
28
- // ── Mocks ─────────────────────────────────────────────────────────────────────
29
-
30
- vi.mock("../storage/history.js", () => ({
31
- getRecentBySenderId: vi.fn(() => []),
32
- }));
33
-
34
- // We do NOT mock ../util/time.js — formatSmartTimestamp is a pure function
35
- // that we want exercised for real so that timestamp output is visible in results.
36
-
37
- // ── Dynamic imports (after mocks) ────────────────────────────────────────────
38
-
39
- const { getRecentBySenderId } = await import("../storage/history.js");
40
- const { enrichDMPrompt, enrichGroupPrompt } =
41
- await import("../core/prompt-builder.js");
42
-
43
- // ── Helpers ──────────────────────────────────────────────────────────────────
44
-
45
- /** Build a minimal history message object. */
46
- function msg(
47
- msgId: number,
48
- senderId: number,
49
- senderName: string,
50
- text: string,
51
- timestamp = Date.now(),
52
- ) {
53
- return { msgId, senderId, senderName, text, timestamp };
54
- }
55
-
56
- // ── enrichDMPrompt ────────────────────────────────────────────────────────────
57
-
58
- describe("enrichDMPrompt — extended edge cases", () => {
59
- it("omits the @tag when username is an empty string (falsy branch)", () => {
60
- // senderUsername = "" → `${senderUsername ? ` (@${senderUsername})` : ""}` → ""
61
- const result = enrichDMPrompt("hello", "Alice", "");
62
- expect(result).toBe("[DM from Alice]\nhello");
63
- expect(result).not.toContain("@");
64
- });
65
-
66
- it("preserves a multi-line prompt body verbatim", () => {
67
- const multiLine = "line one\nline two\nline three";
68
- const result = enrichDMPrompt(multiLine, "Bob");
69
- expect(result).toBe(`[DM from Bob]\n${multiLine}`);
70
- });
71
-
72
- it("handles special characters in sender name", () => {
73
- const result = enrichDMPrompt("hi", "O'Brien & Co.", "obrien");
74
- expect(result).toBe("[DM from O'Brien & Co. (@obrien)]\nhi");
75
- });
76
-
77
- it("handles special characters in username", () => {
78
- const result = enrichDMPrompt("hi", "User", "user.name_123");
79
- expect(result).toBe("[DM from User (@user.name_123)]\nhi");
80
- });
81
-
82
- it("handles an empty prompt string", () => {
83
- const result = enrichDMPrompt("", "Carol");
84
- expect(result).toBe("[DM from Carol]\n");
85
- });
86
-
87
- it("handles a whitespace-only sender name", () => {
88
- // No special handling expected — just passed through
89
- const result = enrichDMPrompt("msg", " ");
90
- expect(result).toBe("[DM from ]\nmsg");
91
- });
92
-
93
- it("format is always [DM from NAME] newline PROMPT", () => {
94
- const name = "TestUser";
95
- const username = "tuser";
96
- const prompt = "test prompt";
97
- const result = enrichDMPrompt(prompt, name, username);
98
- expect(result.startsWith("[DM from TestUser (@tuser)]\n")).toBe(true);
99
- expect(result.endsWith(prompt)).toBe(true);
100
- });
101
- });
102
-
103
- // ── enrichGroupPrompt ─────────────────────────────────────────────────────────
104
-
105
- describe("enrichGroupPrompt — extended branch coverage", () => {
106
- beforeEach(() => {
107
- vi.mocked(getRecentBySenderId).mockReset();
108
- });
109
-
110
- // ── boundary: exactly 2 messages (1 prior + 1 current) ──────────────────
111
-
112
- it("includes context when there are exactly 2 messages (minimum enrichment case)", () => {
113
- vi.mocked(getRecentBySenderId).mockReturnValue([
114
- msg(
115
- 1,
116
- 10,
117
- "Alice",
118
- "prior message",
119
- new Date("2025-06-01T10:00:00Z").getTime(),
120
- ),
121
- msg(
122
- 2,
123
- 10,
124
- "Alice",
125
- "current message",
126
- new Date("2025-06-01T10:01:00Z").getTime(),
127
- ),
128
- ]);
129
- const result = enrichGroupPrompt("current message", "chat-x", 10);
130
- expect(result).toContain("Alice's recent messages in this group:");
131
- expect(result).toContain("prior message");
132
- expect(result).toContain("current message"); // original prompt preserved
133
- // Structure: context block then blank line then prompt
134
- expect(result).toMatch(/\]\n\ncurrent message$/);
135
- });
136
-
137
- // ── senderName from priorMsgs[0], not the current message ───────────────
138
-
139
- it("uses the senderName from the first prior message (priorMsgs[0])", () => {
140
- vi.mocked(getRecentBySenderId).mockReturnValue([
141
- msg(1, 20, "FirstSender", "msg a", Date.now() - 5000),
142
- msg(2, 20, "SecondSender", "msg b", Date.now() - 3000), // prior[1]
143
- msg(3, 20, "ThirdSender", "current msg", Date.now()), // current (slice off)
144
- ]);
145
- const result = enrichGroupPrompt("current msg", "chat-y", 20);
146
- // senderName header should be from index 0 of priorMsgs
147
- expect(result).toContain("FirstSender's recent messages in this group:");
148
- });
149
-
150
- // ── 6 messages: 5 prior + 1 current ─────────────────────────────────────
151
-
152
- it("includes all 5 prior messages when 6 total are returned", () => {
153
- const now = Date.now();
154
- vi.mocked(getRecentBySenderId).mockReturnValue([
155
- msg(1, 30, "Dave", "msg 1", now - 50000),
156
- msg(2, 30, "Dave", "msg 2", now - 40000),
157
- msg(3, 30, "Dave", "msg 3", now - 30000),
158
- msg(4, 30, "Dave", "msg 4", now - 20000),
159
- msg(5, 30, "Dave", "msg 5", now - 10000),
160
- msg(6, 30, "Dave", "current", now),
161
- ]);
162
- const result = enrichGroupPrompt("current", "chat-z", 30);
163
- expect(result).toContain("Dave's recent messages in this group:");
164
- for (let i = 1; i <= 5; i++) {
165
- expect(result).toContain(`msg ${i}`);
166
- }
167
- expect(result).toContain("current"); // original prompt
168
- // The current message's text should NOT appear inside the context block
169
- const contextBlock = result.split("]\n\n")[0];
170
- expect(contextBlock).not.toContain("current");
171
- });
172
-
173
- // ── text truncation at exactly 200 chars ─────────────────────────────────
174
-
175
- it("truncates messages at exactly 200 characters (slice boundary)", () => {
176
- const exactly200 = "a".repeat(200);
177
- const longText = "a".repeat(250);
178
- vi.mocked(getRecentBySenderId).mockReturnValue([
179
- msg(1, 40, "Eve", longText, Date.now() - 2000),
180
- msg(2, 40, "Eve", "current", Date.now()),
181
- ]);
182
- const result = enrichGroupPrompt("current", "chat-trunc", 40);
183
- expect(result).toContain(exactly200);
184
- expect(result).not.toContain("a".repeat(201));
185
- });
186
-
187
- it("short messages (< 200 chars) are not truncated or padded", () => {
188
- const shortText = "short message";
189
- vi.mocked(getRecentBySenderId).mockReturnValue([
190
- msg(1, 41, "Frank", shortText, Date.now() - 1000),
191
- msg(2, 41, "Frank", "current", Date.now()),
192
- ]);
193
- const result = enrichGroupPrompt("current", "chat-short", 41);
194
- expect(result).toContain(shortText);
195
- // The full text appears — no additional characters appended to the message body.
196
- // Note: the outer context bracket `]` is appended after the last context line
197
- // in the format `[header:\n lines]`, so we check containment, not endsWith.
198
- const lines = result.split("\n");
199
- const priorLine = lines.find((l) => l.includes(shortText));
200
- expect(priorLine).toBeDefined();
201
- // Line format: ` [timestamp] short message` optionally followed by `]` (closing bracket)
202
- // Verify the message text appears after the timestamp bracket and is not padded
203
- expect(priorLine).toMatch(new RegExp(`\\] ${shortText}\\]?$`));
204
- });
205
-
206
- // ── getRecentBySenderId called with correct args ──────────────────────────
207
-
208
- it("calls getRecentBySenderId with the provided chatId and senderId", () => {
209
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
210
- enrichGroupPrompt("hi", "specific-chat-id", 99);
211
- expect(getRecentBySenderId).toHaveBeenCalledWith("specific-chat-id", 99, 5);
212
- });
213
-
214
- it("requests exactly 5 recent messages from history", () => {
215
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
216
- enrichGroupPrompt("hi", "chat-count", 55);
217
- const [, , limit] = vi.mocked(getRecentBySenderId).mock.calls[0];
218
- expect(limit).toBe(5);
219
- });
220
-
221
- // ── output structure ──────────────────────────────────────────────────────
222
-
223
- it("context block is wrapped with [ ... ] and separated from prompt by blank line", () => {
224
- vi.mocked(getRecentBySenderId).mockReturnValue([
225
- msg(1, 50, "Gina", "past msg", Date.now() - 3000),
226
- msg(2, 50, "Gina", "now", Date.now()),
227
- ]);
228
- const result = enrichGroupPrompt("now", "chat-struct", 50);
229
- // Should start with '[' (the context header)
230
- expect(result.startsWith("[")).toBe(true);
231
- // Blank line (\n\n) separates context block from the original prompt
232
- expect(result).toContain("]\n\nnow");
233
- });
234
-
235
- it("each prior message line is indented with two spaces", () => {
236
- vi.mocked(getRecentBySenderId).mockReturnValue([
237
- msg(1, 60, "Hank", "indented message", Date.now() - 5000),
238
- msg(2, 60, "Hank", "current", Date.now()),
239
- ]);
240
- const result = enrichGroupPrompt("current", "chat-indent", 60);
241
- // The format is ` [timestamp] message text`
242
- const lines = result.split("\n");
243
- const priorLine = lines.find((l) => l.includes("indented message"));
244
- expect(priorLine).toBeDefined();
245
- expect(priorLine!.startsWith(" [")).toBe(true);
246
- });
247
-
248
- // ── timestamp is formatted and embedded ──────────────────────────────────
249
-
250
- it("includes a formatted timestamp in each prior message line", () => {
251
- const ts = new Date("2020-03-15T12:34:00Z").getTime(); // past year → full date format
252
- vi.mocked(getRecentBySenderId).mockReturnValue([
253
- msg(1, 70, "Iris", "timestamped msg", ts),
254
- msg(2, 70, "Iris", "current", Date.now()),
255
- ]);
256
- const result = enrichGroupPrompt("current", "chat-ts", 70);
257
- // The context block line should contain a bracket-wrapped timestamp
258
- const lines = result.split("\n");
259
- const priorLine = lines.find((l) => l.includes("timestamped msg"));
260
- expect(priorLine).toBeDefined();
261
- // Format: ` [<timestamp>] text` — there should be a timestamp inside brackets
262
- expect(priorLine).toMatch(/\[.+\]/);
263
- });
264
-
265
- // ── returns prompt unchanged for 0 and 1 message (re-confirmed at boundary)
266
-
267
- it("returns the prompt unchanged when getRecentBySenderId returns empty array", () => {
268
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
269
- const result = enrichGroupPrompt("only message", "chat-empty", 1);
270
- expect(result).toBe("only message");
271
- });
272
-
273
- it("returns the prompt unchanged when exactly 1 message is returned (the current)", () => {
274
- vi.mocked(getRecentBySenderId).mockReturnValue([
275
- msg(1, 80, "Jan", "the only message", Date.now()),
276
- ]);
277
- const result = enrichGroupPrompt("the only message", "chat-one", 80);
278
- expect(result).toBe("the only message");
279
- });
280
-
281
- // ── multi-line prompt body is preserved ───────────────────────────────────
282
-
283
- it("preserves a multi-line original prompt after the context block", () => {
284
- vi.mocked(getRecentBySenderId).mockReturnValue([
285
- msg(1, 90, "Karl", "prior", Date.now() - 1000),
286
- msg(2, 90, "Karl", "current line 1\ncurrent line 2", Date.now()),
287
- ]);
288
- const result = enrichGroupPrompt(
289
- "current line 1\ncurrent line 2",
290
- "chat-ml",
291
- 90,
292
- );
293
- expect(result).toContain("current line 1\ncurrent line 2");
294
- expect(result.endsWith("current line 1\ncurrent line 2")).toBe(true);
295
- });
296
- });
@@ -1,106 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- vi.mock("../storage/history.js", () => ({
4
- getRecentBySenderId: vi.fn(() => []),
5
- }));
6
-
7
- const { getRecentBySenderId } = await import("../storage/history.js");
8
- const { enrichDMPrompt, enrichGroupPrompt } =
9
- await import("../core/prompt-builder.js");
10
-
11
- describe("enrichDMPrompt", () => {
12
- it("prepends DM metadata", () => {
13
- const result = enrichDMPrompt("hello", "Alice");
14
- expect(result).toBe("[DM from Alice]\nhello");
15
- });
16
-
17
- it("includes username when provided", () => {
18
- const result = enrichDMPrompt("hello", "Alice", "alice42");
19
- expect(result).toBe("[DM from Alice (@alice42)]\nhello");
20
- });
21
-
22
- it("works without username", () => {
23
- const result = enrichDMPrompt("hello", "Bob", undefined);
24
- expect(result).toBe("[DM from Bob]\nhello");
25
- });
26
- });
27
-
28
- describe("enrichGroupPrompt", () => {
29
- beforeEach(() => {
30
- vi.mocked(getRecentBySenderId).mockReset();
31
- });
32
-
33
- it("returns prompt unchanged when no prior messages", () => {
34
- vi.mocked(getRecentBySenderId).mockReturnValue([]);
35
- const result = enrichGroupPrompt("hello", "chat1", 42);
36
- expect(result).toBe("hello");
37
- });
38
-
39
- it("returns prompt unchanged when only one message (current)", () => {
40
- vi.mocked(getRecentBySenderId).mockReturnValue([
41
- {
42
- msgId: 1,
43
- senderId: 42,
44
- senderName: "Alice",
45
- text: "hello",
46
- timestamp: Date.now(),
47
- },
48
- ]);
49
- const result = enrichGroupPrompt("hello", "chat1", 42);
50
- expect(result).toBe("hello");
51
- });
52
-
53
- it("prepends prior messages for threading context", () => {
54
- vi.mocked(getRecentBySenderId).mockReturnValue([
55
- {
56
- msgId: 1,
57
- senderId: 42,
58
- senderName: "Alice",
59
- text: "first message",
60
- timestamp: new Date("2025-01-01T10:00:00Z").getTime(),
61
- },
62
- {
63
- msgId: 2,
64
- senderId: 42,
65
- senderName: "Alice",
66
- text: "second message",
67
- timestamp: new Date("2025-01-01T10:01:00Z").getTime(),
68
- },
69
- {
70
- msgId: 3,
71
- senderId: 42,
72
- senderName: "Alice",
73
- text: "current",
74
- timestamp: new Date("2025-01-01T10:02:00Z").getTime(),
75
- },
76
- ]);
77
- const result = enrichGroupPrompt("current", "chat1", 42);
78
- expect(result).toContain("Alice's recent messages");
79
- expect(result).toContain("first message");
80
- expect(result).toContain("second message");
81
- expect(result).toContain("current"); // the original prompt
82
- });
83
-
84
- it("truncates long messages to 200 chars", () => {
85
- const longText = "x".repeat(300);
86
- vi.mocked(getRecentBySenderId).mockReturnValue([
87
- {
88
- msgId: 1,
89
- senderId: 42,
90
- senderName: "Bob",
91
- text: longText,
92
- timestamp: Date.now(),
93
- },
94
- {
95
- msgId: 2,
96
- senderId: 42,
97
- senderName: "Bob",
98
- text: "current",
99
- timestamp: Date.now(),
100
- },
101
- ]);
102
- const result = enrichGroupPrompt("current", "chat1", 42);
103
- expect(result).not.toContain(longText); // full text shouldn't appear
104
- expect(result).toContain("x".repeat(200)); // truncated version should
105
- });
106
- });
@@ -1,40 +0,0 @@
1
- /**
2
- * Prompt enrichment — adds context to raw user messages before sending to the AI.
3
- * Platform-agnostic; works with any messaging frontend.
4
- */
5
-
6
- import { getRecentBySenderId } from "../storage/history.js";
7
- import { formatSmartTimestamp } from "../util/time.js";
8
-
9
- /**
10
- * Enrich a DM prompt with sender metadata.
11
- */
12
- export function enrichDMPrompt(
13
- prompt: string,
14
- senderName: string,
15
- senderUsername?: string,
16
- ): string {
17
- const userTag = senderUsername ? ` (@${senderUsername})` : "";
18
- return `[DM from ${senderName}${userTag}]\n${prompt}`;
19
- }
20
-
21
- /**
22
- * Enrich a group prompt with the sender's recent messages for threading context.
23
- */
24
- export function enrichGroupPrompt(
25
- prompt: string,
26
- chatId: string,
27
- senderId: number,
28
- ): string {
29
- const recentMsgs = getRecentBySenderId(chatId, senderId, 5);
30
- if (recentMsgs.length <= 1) return prompt;
31
-
32
- const priorMsgs = recentMsgs.slice(0, -1);
33
- const senderName = priorMsgs[0].senderName;
34
- const contextLines = priorMsgs
35
- .map(
36
- (m) => ` [${formatSmartTimestamp(m.timestamp)}] ${m.text.slice(0, 200)}`,
37
- )
38
- .join("\n");
39
- return `[${senderName}'s recent messages in this group:\n${contextLines}]\n\n${prompt}`;
40
- }