talon-agent 1.0.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 +137 -0
- package/bin/talon.js +5 -0
- package/package.json +86 -0
- package/prompts/base.md +13 -0
- package/prompts/custom.md.example +22 -0
- package/prompts/dream.md +41 -0
- package/prompts/identity.md +45 -0
- package/prompts/teams.md +52 -0
- package/prompts/telegram.md +89 -0
- package/prompts/terminal.md +13 -0
- package/src/__tests__/chat-id.test.ts +91 -0
- package/src/__tests__/chat-settings.test.ts +337 -0
- package/src/__tests__/config.test.ts +546 -0
- package/src/__tests__/cron-store.test.ts +440 -0
- package/src/__tests__/daily-log.test.ts +146 -0
- package/src/__tests__/dispatcher.test.ts +383 -0
- package/src/__tests__/errors.test.ts +240 -0
- package/src/__tests__/fuzz.test.ts +302 -0
- package/src/__tests__/gateway-actions.test.ts +1453 -0
- package/src/__tests__/gateway-context.test.ts +102 -0
- package/src/__tests__/gateway-http.test.ts +245 -0
- package/src/__tests__/handlers.test.ts +351 -0
- package/src/__tests__/history-persistence.test.ts +172 -0
- package/src/__tests__/history.test.ts +659 -0
- package/src/__tests__/integration.test.ts +189 -0
- package/src/__tests__/log.test.ts +110 -0
- package/src/__tests__/media-index.test.ts +277 -0
- package/src/__tests__/plugin.test.ts +317 -0
- package/src/__tests__/prompt-builder.test.ts +71 -0
- package/src/__tests__/sessions.test.ts +594 -0
- package/src/__tests__/teams-frontend.test.ts +239 -0
- package/src/__tests__/telegram.test.ts +177 -0
- package/src/__tests__/terminal-commands.test.ts +367 -0
- package/src/__tests__/terminal-frontend.test.ts +141 -0
- package/src/__tests__/terminal-renderer.test.ts +278 -0
- package/src/__tests__/watchdog.test.ts +287 -0
- package/src/__tests__/workspace.test.ts +184 -0
- package/src/backend/claude-sdk/index.ts +438 -0
- package/src/backend/claude-sdk/tools.ts +605 -0
- package/src/backend/opencode/index.ts +252 -0
- package/src/bootstrap.ts +134 -0
- package/src/cli.ts +611 -0
- package/src/core/cron.ts +148 -0
- package/src/core/dispatcher.ts +126 -0
- package/src/core/dream.ts +295 -0
- package/src/core/errors.ts +206 -0
- package/src/core/gateway-actions.ts +267 -0
- package/src/core/gateway.ts +258 -0
- package/src/core/plugin.ts +432 -0
- package/src/core/prompt-builder.ts +43 -0
- package/src/core/pulse.ts +175 -0
- package/src/core/types.ts +85 -0
- package/src/frontend/teams/actions.ts +101 -0
- package/src/frontend/teams/formatting.ts +220 -0
- package/src/frontend/teams/graph.ts +297 -0
- package/src/frontend/teams/index.ts +308 -0
- package/src/frontend/teams/proxy-fetch.ts +28 -0
- package/src/frontend/teams/tools.ts +177 -0
- package/src/frontend/telegram/actions.ts +437 -0
- package/src/frontend/telegram/admin.ts +178 -0
- package/src/frontend/telegram/callbacks.ts +251 -0
- package/src/frontend/telegram/commands.ts +543 -0
- package/src/frontend/telegram/formatting.ts +101 -0
- package/src/frontend/telegram/handlers.ts +1008 -0
- package/src/frontend/telegram/helpers.ts +105 -0
- package/src/frontend/telegram/index.ts +130 -0
- package/src/frontend/telegram/middleware.ts +177 -0
- package/src/frontend/telegram/userbot.ts +546 -0
- package/src/frontend/terminal/commands.ts +303 -0
- package/src/frontend/terminal/index.ts +282 -0
- package/src/frontend/terminal/input.ts +297 -0
- package/src/frontend/terminal/renderer.ts +248 -0
- package/src/index.ts +144 -0
- package/src/login.ts +89 -0
- package/src/storage/chat-settings.ts +218 -0
- package/src/storage/cron-store.ts +165 -0
- package/src/storage/daily-log.ts +97 -0
- package/src/storage/history.ts +278 -0
- package/src/storage/media-index.ts +116 -0
- package/src/storage/sessions.ts +328 -0
- package/src/util/chat-id.ts +21 -0
- package/src/util/config.ts +244 -0
- package/src/util/log.ts +122 -0
- package/src/util/paths.ts +80 -0
- package/src/util/time.ts +86 -0
- package/src/util/trace.ts +35 -0
- package/src/util/watchdog.ts +108 -0
- package/src/util/workspace.ts +208 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../util/log.js", () => ({
|
|
4
|
+
log: vi.fn(),
|
|
5
|
+
logError: vi.fn(),
|
|
6
|
+
logWarn: vi.fn(),
|
|
7
|
+
logDebug: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("plugin system", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
for (const key of Object.keys(process.env)) {
|
|
14
|
+
if (key.startsWith("TEST_PLUGIN_")) delete process.env[key];
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function createMockPlugin(overrides: Record<string, unknown> = {}) {
|
|
19
|
+
return {
|
|
20
|
+
name: "test-plugin",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
description: "Test plugin",
|
|
23
|
+
mcpServerPath: "/fake/tools.ts",
|
|
24
|
+
getEnvVars: vi.fn(() => ({ TEST_PLUGIN_KEY: "value" })),
|
|
25
|
+
handleAction: vi.fn(async () => null),
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Import fresh plugin module with fs mocked to find entry + dynamic import returning plugin. */
|
|
31
|
+
async function setup(plugin: ReturnType<typeof createMockPlugin>) {
|
|
32
|
+
vi.doMock("node:fs", () => ({ existsSync: vi.fn(() => true) }));
|
|
33
|
+
const mod = await import("../core/plugin.js");
|
|
34
|
+
mod._deps.importModule = async () => ({ default: plugin });
|
|
35
|
+
return mod;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("PluginRegistry", () => {
|
|
39
|
+
it("registers a plugin and retrieves it by name", async () => {
|
|
40
|
+
const plugin = createMockPlugin();
|
|
41
|
+
const { loadPlugins, getPlugin, getPluginCount } = await setup(plugin);
|
|
42
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
43
|
+
|
|
44
|
+
expect(getPluginCount()).toBe(1);
|
|
45
|
+
expect(getPlugin("test-plugin")).toBeDefined();
|
|
46
|
+
expect(getPlugin("test-plugin")!.plugin.name).toBe("test-plugin");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("prevents duplicate plugin names", async () => {
|
|
50
|
+
const plugin = createMockPlugin();
|
|
51
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
52
|
+
await loadPlugins([{ path: "/fake/plugin" }, { path: "/fake/plugin" }]);
|
|
53
|
+
|
|
54
|
+
expect(getPluginCount()).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("skips plugins with no entry point", async () => {
|
|
58
|
+
vi.doMock("node:fs", () => ({ existsSync: vi.fn(() => false) }));
|
|
59
|
+
const { loadPlugins, getPluginCount } = await import("../core/plugin.js");
|
|
60
|
+
await loadPlugins([{ path: "/nonexistent/plugin" }]);
|
|
61
|
+
|
|
62
|
+
expect(getPluginCount()).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("skips plugins missing name", async () => {
|
|
66
|
+
vi.doMock("node:fs", () => ({ existsSync: vi.fn(() => true) }));
|
|
67
|
+
const mod = await import("../core/plugin.js");
|
|
68
|
+
mod._deps.importModule = async () => ({
|
|
69
|
+
default: { handleAction: vi.fn() },
|
|
70
|
+
});
|
|
71
|
+
await mod.loadPlugins([{ path: "/fake/plugin" }]);
|
|
72
|
+
|
|
73
|
+
expect(mod.getPluginCount()).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("config validation", () => {
|
|
78
|
+
it("skips plugin when validateConfig returns errors", async () => {
|
|
79
|
+
const plugin = createMockPlugin({
|
|
80
|
+
validateConfig: () => [
|
|
81
|
+
"repoPath is required",
|
|
82
|
+
"jenkinsUrl is required",
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
86
|
+
await loadPlugins([{ path: "/fake/plugin", config: {} }]);
|
|
87
|
+
|
|
88
|
+
expect(getPluginCount()).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("loads plugin when validateConfig returns undefined (valid)", async () => {
|
|
92
|
+
const plugin = createMockPlugin({ validateConfig: () => undefined });
|
|
93
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
94
|
+
await loadPlugins([
|
|
95
|
+
{ path: "/fake/plugin", config: { repoPath: "/tmp" } },
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(getPluginCount()).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("env vars", () => {
|
|
103
|
+
it("sets env vars from getEnvVars on process.env", async () => {
|
|
104
|
+
const plugin = createMockPlugin({
|
|
105
|
+
getEnvVars: () => ({ TEST_PLUGIN_FOO: "bar", TEST_PLUGIN_BAZ: "qux" }),
|
|
106
|
+
});
|
|
107
|
+
const { loadPlugins } = await setup(plugin);
|
|
108
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
109
|
+
|
|
110
|
+
expect(process.env.TEST_PLUGIN_FOO).toBe("bar");
|
|
111
|
+
expect(process.env.TEST_PLUGIN_BAZ).toBe("qux");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("action routing", () => {
|
|
116
|
+
it("routes action to the correct plugin", async () => {
|
|
117
|
+
const plugin = createMockPlugin({
|
|
118
|
+
handleAction: vi.fn(async (body: Record<string, unknown>) => {
|
|
119
|
+
if (body.action === "test_action")
|
|
120
|
+
return { ok: true, text: "handled" };
|
|
121
|
+
return null;
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const { loadPlugins, handlePluginAction } = await setup(plugin);
|
|
125
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
126
|
+
|
|
127
|
+
const result = await handlePluginAction(
|
|
128
|
+
{ action: "test_action" },
|
|
129
|
+
"chat1",
|
|
130
|
+
);
|
|
131
|
+
expect(result).toEqual({ ok: true, text: "handled" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns null when no plugin handles the action", async () => {
|
|
135
|
+
const plugin = createMockPlugin({
|
|
136
|
+
handleAction: vi.fn(async () => null),
|
|
137
|
+
});
|
|
138
|
+
const { loadPlugins, handlePluginAction } = await setup(plugin);
|
|
139
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
140
|
+
|
|
141
|
+
const result = await handlePluginAction({ action: "unknown" }, "chat1");
|
|
142
|
+
expect(result).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("catches plugin action errors and returns error result", async () => {
|
|
146
|
+
const plugin = createMockPlugin({
|
|
147
|
+
handleAction: vi.fn(async () => {
|
|
148
|
+
throw new Error("plugin crash");
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const { loadPlugins, handlePluginAction } = await setup(plugin);
|
|
152
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
153
|
+
|
|
154
|
+
const result = await handlePluginAction({ action: "boom" }, "chat1");
|
|
155
|
+
expect(result?.ok).toBe(false);
|
|
156
|
+
expect(result?.error).toContain("plugin crash");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("skips plugins without handleAction", async () => {
|
|
160
|
+
const plugin = createMockPlugin();
|
|
161
|
+
delete (plugin as Record<string, unknown>).handleAction;
|
|
162
|
+
const { loadPlugins, handlePluginAction } = await setup(plugin);
|
|
163
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
164
|
+
|
|
165
|
+
const result = await handlePluginAction({ action: "test" }, "chat1");
|
|
166
|
+
expect(result).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("MCP server config", () => {
|
|
171
|
+
it("builds MCP server entries for plugins with mcpServerPath", async () => {
|
|
172
|
+
const plugin = createMockPlugin({
|
|
173
|
+
mcpServerPath: "/fake/tools.ts",
|
|
174
|
+
getEnvVars: () => ({ MY_KEY: "val" }),
|
|
175
|
+
});
|
|
176
|
+
const { loadPlugins, getPluginMcpServers } = await setup(plugin);
|
|
177
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
178
|
+
|
|
179
|
+
const servers = getPluginMcpServers("http://localhost:19876", "chat1");
|
|
180
|
+
expect(servers["test-plugin-tools"]).toBeDefined();
|
|
181
|
+
expect(servers["test-plugin-tools"].env.TALON_BRIDGE_URL).toBe(
|
|
182
|
+
"http://localhost:19876",
|
|
183
|
+
);
|
|
184
|
+
expect(servers["test-plugin-tools"].env.TALON_CHAT_ID).toBe("chat1");
|
|
185
|
+
expect(servers["test-plugin-tools"].env.MY_KEY).toBe("val");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("skips plugins without mcpServerPath", async () => {
|
|
189
|
+
const plugin = createMockPlugin();
|
|
190
|
+
delete (plugin as Record<string, unknown>).mcpServerPath;
|
|
191
|
+
const { loadPlugins, getPluginMcpServers } = await setup(plugin);
|
|
192
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
193
|
+
|
|
194
|
+
const servers = getPluginMcpServers("http://localhost:19876", "chat1");
|
|
195
|
+
expect(Object.keys(servers)).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("system prompt additions", () => {
|
|
200
|
+
it("collects prompt additions from plugins", async () => {
|
|
201
|
+
const plugin = createMockPlugin({
|
|
202
|
+
getSystemPromptAddition: () => "## My Plugin\nI add context.",
|
|
203
|
+
});
|
|
204
|
+
const { loadPlugins, getPluginPromptAdditions } = await setup(plugin);
|
|
205
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
206
|
+
|
|
207
|
+
const additions = getPluginPromptAdditions();
|
|
208
|
+
expect(additions).toHaveLength(1);
|
|
209
|
+
expect(additions[0]).toContain("My Plugin");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("handles plugins without getSystemPromptAddition", async () => {
|
|
213
|
+
const plugin = createMockPlugin();
|
|
214
|
+
delete (plugin as Record<string, unknown>).getSystemPromptAddition;
|
|
215
|
+
const { loadPlugins, getPluginPromptAdditions } = await setup(plugin);
|
|
216
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
217
|
+
|
|
218
|
+
expect(getPluginPromptAdditions()).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("lifecycle", () => {
|
|
223
|
+
it("calls init with config", async () => {
|
|
224
|
+
const initFn = vi.fn();
|
|
225
|
+
const plugin = createMockPlugin({ init: initFn });
|
|
226
|
+
const { loadPlugins } = await setup(plugin);
|
|
227
|
+
await loadPlugins([{ path: "/fake/plugin", config: { key: "val" } }]);
|
|
228
|
+
|
|
229
|
+
expect(initFn).toHaveBeenCalledWith({ key: "val" });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("calls destroy on all plugins", async () => {
|
|
233
|
+
const destroyFn = vi.fn();
|
|
234
|
+
const plugin = createMockPlugin({ destroy: destroyFn });
|
|
235
|
+
const { loadPlugins, destroyPlugins } = await setup(plugin);
|
|
236
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
237
|
+
await destroyPlugins();
|
|
238
|
+
|
|
239
|
+
expect(destroyFn).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("still registers plugin if init throws", async () => {
|
|
243
|
+
const plugin = createMockPlugin({
|
|
244
|
+
init: () => {
|
|
245
|
+
throw new Error("init failed");
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
249
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
250
|
+
|
|
251
|
+
expect(getPluginCount()).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("catches destroy errors without crashing", async () => {
|
|
255
|
+
const plugin = createMockPlugin({
|
|
256
|
+
destroy: () => {
|
|
257
|
+
throw new Error("destroy crash");
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const { loadPlugins, destroyPlugins } = await setup(plugin);
|
|
261
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
262
|
+
|
|
263
|
+
await expect(destroyPlugins()).resolves.toBeUndefined();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("frontend whitelist", () => {
|
|
268
|
+
it("skips plugin when frontend whitelist doesn't match", async () => {
|
|
269
|
+
const plugin = createMockPlugin({ frontends: ["telegram"] });
|
|
270
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
271
|
+
await loadPlugins([{ path: "/fake/plugin" }], ["terminal"]);
|
|
272
|
+
|
|
273
|
+
expect(getPluginCount()).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("loads plugin when frontend whitelist matches", async () => {
|
|
277
|
+
const plugin = createMockPlugin({ frontends: ["telegram", "terminal"] });
|
|
278
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
279
|
+
await loadPlugins([{ path: "/fake/plugin" }], ["terminal"]);
|
|
280
|
+
|
|
281
|
+
expect(getPluginCount()).toBe(1);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("loads plugin when no frontend whitelist is specified", async () => {
|
|
285
|
+
const plugin = createMockPlugin();
|
|
286
|
+
delete (plugin as Record<string, unknown>).frontends;
|
|
287
|
+
const { loadPlugins, getPluginCount } = await setup(plugin);
|
|
288
|
+
await loadPlugins([{ path: "/fake/plugin" }], ["terminal"]);
|
|
289
|
+
|
|
290
|
+
expect(getPluginCount()).toBe(1);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("prompt addition error handling", () => {
|
|
295
|
+
it("catches getSystemPromptAddition errors without crashing", async () => {
|
|
296
|
+
const plugin = createMockPlugin({
|
|
297
|
+
getSystemPromptAddition: () => {
|
|
298
|
+
throw new Error("prompt error");
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
const { loadPlugins, getPluginPromptAdditions } = await setup(plugin);
|
|
302
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
303
|
+
|
|
304
|
+
expect(getPluginPromptAdditions()).toHaveLength(0);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("skips empty/whitespace prompt additions", async () => {
|
|
308
|
+
const plugin = createMockPlugin({
|
|
309
|
+
getSystemPromptAddition: () => " \n ",
|
|
310
|
+
});
|
|
311
|
+
const { loadPlugins, getPluginPromptAdditions } = await setup(plugin);
|
|
312
|
+
await loadPlugins([{ path: "/fake/plugin" }]);
|
|
313
|
+
|
|
314
|
+
expect(getPluginPromptAdditions()).toHaveLength(0);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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 } = await import(
|
|
9
|
+
"../core/prompt-builder.js"
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe("enrichDMPrompt", () => {
|
|
13
|
+
it("prepends DM metadata", () => {
|
|
14
|
+
const result = enrichDMPrompt("hello", "Alice");
|
|
15
|
+
expect(result).toBe("[DM from Alice]\nhello");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("includes username when provided", () => {
|
|
19
|
+
const result = enrichDMPrompt("hello", "Alice", "alice42");
|
|
20
|
+
expect(result).toBe("[DM from Alice (@alice42)]\nhello");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("works without username", () => {
|
|
24
|
+
const result = enrichDMPrompt("hello", "Bob", undefined);
|
|
25
|
+
expect(result).toBe("[DM from Bob]\nhello");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("enrichGroupPrompt", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.mocked(getRecentBySenderId).mockReset();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns prompt unchanged when no prior messages", () => {
|
|
35
|
+
vi.mocked(getRecentBySenderId).mockReturnValue([]);
|
|
36
|
+
const result = enrichGroupPrompt("hello", "chat1", 42);
|
|
37
|
+
expect(result).toBe("hello");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns prompt unchanged when only one message (current)", () => {
|
|
41
|
+
vi.mocked(getRecentBySenderId).mockReturnValue([
|
|
42
|
+
{ msgId: 1, senderId: 42, senderName: "Alice", text: "hello", timestamp: Date.now() },
|
|
43
|
+
]);
|
|
44
|
+
const result = enrichGroupPrompt("hello", "chat1", 42);
|
|
45
|
+
expect(result).toBe("hello");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prepends prior messages for threading context", () => {
|
|
49
|
+
vi.mocked(getRecentBySenderId).mockReturnValue([
|
|
50
|
+
{ msgId: 1, senderId: 42, senderName: "Alice", text: "first message", timestamp: new Date("2025-01-01T10:00:00Z").getTime() },
|
|
51
|
+
{ msgId: 2, senderId: 42, senderName: "Alice", text: "second message", timestamp: new Date("2025-01-01T10:01:00Z").getTime() },
|
|
52
|
+
{ msgId: 3, senderId: 42, senderName: "Alice", text: "current", timestamp: new Date("2025-01-01T10:02:00Z").getTime() },
|
|
53
|
+
]);
|
|
54
|
+
const result = enrichGroupPrompt("current", "chat1", 42);
|
|
55
|
+
expect(result).toContain("Alice's recent messages");
|
|
56
|
+
expect(result).toContain("first message");
|
|
57
|
+
expect(result).toContain("second message");
|
|
58
|
+
expect(result).toContain("current"); // the original prompt
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("truncates long messages to 200 chars", () => {
|
|
62
|
+
const longText = "x".repeat(300);
|
|
63
|
+
vi.mocked(getRecentBySenderId).mockReturnValue([
|
|
64
|
+
{ msgId: 1, senderId: 42, senderName: "Bob", text: longText, timestamp: Date.now() },
|
|
65
|
+
{ msgId: 2, senderId: 42, senderName: "Bob", text: "current", timestamp: Date.now() },
|
|
66
|
+
]);
|
|
67
|
+
const result = enrichGroupPrompt("current", "chat1", 42);
|
|
68
|
+
expect(result).not.toContain(longText); // full text shouldn't appear
|
|
69
|
+
expect(result).toContain("x".repeat(200)); // truncated version should
|
|
70
|
+
});
|
|
71
|
+
});
|