arisa 5.1.49 → 5.1.60
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/AGENTS.md +0 -2
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +39 -489
- package/src/core/agent/agent-session-lifecycle.js +181 -0
- package/src/core/agent/pi-capability-tools.js +183 -0
- package/src/core/artifacts/artifact-store.js +73 -17
- package/src/core/capabilities/capability-service.js +340 -0
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +53 -0
- package/src/core/tasks/task-store.js +316 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/official-tools.lock.json +7 -4
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +10 -1
- package/src/runtime/create-headless-app.js +5 -2
- package/src/transport/telegram/bot.js +112 -368
- package/src/transport/telegram/chat-queue.js +72 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/task-dispatcher.js +73 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +170 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/test/agent-session-lifecycle.test.js +58 -0
- package/test/artifact-store.test.js +38 -2
- package/test/capabilities-security.test.js +58 -0
- package/test/context-and-task-bounds.test.js +76 -1
- package/test/device-code-message.test.js +9 -0
- package/test/media-caption.test.js +1 -1
- package/test/pi-capability-tools.test.js +65 -0
- package/test/session-start-operational-notes.test.js +1 -1
- package/test/task-idempotency.test.js +40 -0
- package/test/task-routing.test.js +62 -0
- package/test/task-store.test.js +178 -6
- package/test/telegram-task-dispatcher.test.js +99 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
package/test/task-store.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -9,7 +9,7 @@ process.env.HOME = homeDir;
|
|
|
9
9
|
process.env.USERPROFILE = homeDir;
|
|
10
10
|
|
|
11
11
|
const { TaskStore } = await import("../src/core/tasks/task-store.js");
|
|
12
|
-
const { arisaHomeDir } = await import("../src/runtime/paths.js");
|
|
12
|
+
const { arisaHomeDir, tasksFile } = await import("../src/runtime/paths.js");
|
|
13
13
|
|
|
14
14
|
async function resetHome() {
|
|
15
15
|
await rm(arisaHomeDir, { recursive: true, force: true });
|
|
@@ -78,15 +78,17 @@ test("recovers interrupted running tasks for retry after restart", async () => {
|
|
|
78
78
|
const recovered = await store.recoverInterrupted();
|
|
79
79
|
|
|
80
80
|
assert.deepEqual(recovered.map((task) => task.id), ["interrupted-once", "interrupted-recurring"]);
|
|
81
|
-
assert.
|
|
82
|
-
assert.
|
|
81
|
+
assert.equal(recovered[0].status, "outcome_uncertain");
|
|
82
|
+
assert.equal(recovered[1].status, "pending");
|
|
83
|
+
assert.ok(Date.parse(recovered[1].runAt) > Date.now());
|
|
84
|
+
assert.ok(recovered.every((task) => task.lastError === "execution interrupted before confirmation"));
|
|
83
85
|
assert.equal((await store.get("still-pending")).status, "pending");
|
|
84
86
|
assert.equal((await store.get("already-done")).status, "done");
|
|
85
87
|
|
|
86
88
|
const restartedStore = new TaskStore();
|
|
87
89
|
assert.deepEqual(
|
|
88
90
|
(await restartedStore.claimDue()).map((task) => task.id),
|
|
89
|
-
["
|
|
91
|
+
["still-pending"]
|
|
90
92
|
);
|
|
91
93
|
});
|
|
92
94
|
|
|
@@ -114,6 +116,175 @@ test("completes one-off tasks and re-schedules recurring interval tasks", async
|
|
|
114
116
|
assert.ok(repeat.lastRunAt);
|
|
115
117
|
});
|
|
116
118
|
|
|
119
|
+
test("compacts terminal payloads while preserving routing and audit identifiers", async () => {
|
|
120
|
+
await resetHome();
|
|
121
|
+
const store = new TaskStore();
|
|
122
|
+
const route = { transport: "telegram", destination: { chatId: -1001, threadId: 87 } };
|
|
123
|
+
const payload = {
|
|
124
|
+
chatId: "chat-1",
|
|
125
|
+
prompt: "private operational prompt ".repeat(200),
|
|
126
|
+
args: { cursor: "large private state".repeat(100) },
|
|
127
|
+
acknowledgement: "received",
|
|
128
|
+
toolName: "checker",
|
|
129
|
+
resourceId: "inbox:primary",
|
|
130
|
+
artifactId: "artifact-1"
|
|
131
|
+
};
|
|
132
|
+
await store.add({ id: "compact-done", kind: "agent_task", payload, route });
|
|
133
|
+
await store.add({ id: "compact-failed", kind: "agent_task", payload });
|
|
134
|
+
await store.add({ id: "compact-uncertain", kind: "agent_task", payload });
|
|
135
|
+
|
|
136
|
+
const done = await store.complete("compact-done");
|
|
137
|
+
const failed = await store.fail("compact-failed", "permanent failure");
|
|
138
|
+
const uncertain = await store.retryOrFail("compact-uncertain", "unknown outcome", {
|
|
139
|
+
retryable: false,
|
|
140
|
+
outcomeUncertain: true
|
|
141
|
+
});
|
|
142
|
+
const expectedPayload = {
|
|
143
|
+
chatId: "chat-1",
|
|
144
|
+
toolName: "checker",
|
|
145
|
+
resourceId: "inbox:primary",
|
|
146
|
+
artifactId: "artifact-1"
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
for (const task of [done, failed, uncertain]) {
|
|
150
|
+
assert.deepEqual(task.payload, expectedPayload);
|
|
151
|
+
assert.equal(task.payloadCompacted, true);
|
|
152
|
+
assert.equal(task.retry, undefined);
|
|
153
|
+
assert.equal(task.recurrence, undefined);
|
|
154
|
+
}
|
|
155
|
+
assert.deepEqual(done.route, route);
|
|
156
|
+
assert.ok(done.completedAt);
|
|
157
|
+
assert.equal(failed.error, "permanent failure");
|
|
158
|
+
assert.ok(uncertain.uncertainAt);
|
|
159
|
+
assert.deepEqual(
|
|
160
|
+
(await store.list({ chatId: "chat-1" })).map((task) => task.id),
|
|
161
|
+
["compact-done", "compact-failed", "compact-uncertain"]
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("startup recovery compacts historical terminal tasks without changing active payloads", async () => {
|
|
166
|
+
await resetHome();
|
|
167
|
+
await mkdir(path.dirname(tasksFile), { recursive: true });
|
|
168
|
+
await writeFile(tasksFile, `${JSON.stringify([
|
|
169
|
+
{
|
|
170
|
+
id: "historical-done",
|
|
171
|
+
kind: "agent_task",
|
|
172
|
+
status: "done",
|
|
173
|
+
payload: { chatId: "chat-1", prompt: "obsolete prompt", args: { large: "value" } },
|
|
174
|
+
retry: { maxAttempts: 3 },
|
|
175
|
+
recurrence: null
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: "active-pending",
|
|
179
|
+
kind: "agent_task",
|
|
180
|
+
status: "pending",
|
|
181
|
+
runAt: new Date(Date.now() + 60_000).toISOString(),
|
|
182
|
+
payload: { chatId: "chat-1", prompt: "still required" }
|
|
183
|
+
}
|
|
184
|
+
], null, 2)}\n`, "utf8");
|
|
185
|
+
|
|
186
|
+
const store = new TaskStore();
|
|
187
|
+
assert.deepEqual(await store.recoverInterrupted(), []);
|
|
188
|
+
const persisted = JSON.parse(await readFile(tasksFile, "utf8"));
|
|
189
|
+
const historical = persisted.find((task) => task.id === "historical-done");
|
|
190
|
+
const active = persisted.find((task) => task.id === "active-pending");
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(historical.payload, { chatId: "chat-1" });
|
|
193
|
+
assert.equal(historical.payloadCompacted, true);
|
|
194
|
+
assert.equal(historical.retry, undefined);
|
|
195
|
+
assert.equal(historical.recurrence, undefined);
|
|
196
|
+
assert.equal(active.payload.prompt, "still required");
|
|
197
|
+
assert.ok(active.retry);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("backs off known failures and fails after the attempt limit", async () => {
|
|
201
|
+
await resetHome();
|
|
202
|
+
const store = new TaskStore();
|
|
203
|
+
const runAt = new Date(Date.now() - 1000).toISOString();
|
|
204
|
+
await store.add({
|
|
205
|
+
id: "retrying",
|
|
206
|
+
kind: "agent_task",
|
|
207
|
+
runAt,
|
|
208
|
+
retry: { maxAttempts: 2, baseDelaySeconds: 1, maxDelaySeconds: 10, multiplier: 2 }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await store.claimDue();
|
|
212
|
+
const retrying = await store.retryOrFail("retrying", "temporary");
|
|
213
|
+
assert.equal(retrying.status, "pending");
|
|
214
|
+
assert.equal(retrying.attempts, 1);
|
|
215
|
+
assert.ok(Date.parse(retrying.runAt) > Date.now());
|
|
216
|
+
|
|
217
|
+
retrying.runAt = runAt;
|
|
218
|
+
store.tasks.find((task) => task.id === "retrying").runAt = runAt;
|
|
219
|
+
await store.save();
|
|
220
|
+
await store.claimDue();
|
|
221
|
+
const failed = await store.retryOrFail("retrying", "still broken");
|
|
222
|
+
assert.equal(failed.status, "failed");
|
|
223
|
+
assert.equal(failed.attempts, 2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("keeps recurring tasks scheduled after a run exhausts its retries", async () => {
|
|
227
|
+
await resetHome();
|
|
228
|
+
const store = new TaskStore();
|
|
229
|
+
const past = new Date(Date.now() - 1000).toISOString();
|
|
230
|
+
await store.add({
|
|
231
|
+
id: "recurring-failure",
|
|
232
|
+
kind: "agent_task",
|
|
233
|
+
runAt: past,
|
|
234
|
+
recurrence: { type: "interval", everySeconds: 60 },
|
|
235
|
+
retry: { maxAttempts: 1 }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await store.claimDue();
|
|
239
|
+
const result = await store.retryOrFail("recurring-failure", "temporary outage");
|
|
240
|
+
|
|
241
|
+
assert.equal(result.status, "pending");
|
|
242
|
+
assert.equal(result.terminalFailure, true);
|
|
243
|
+
assert.equal(result.lastOutcome, "failed");
|
|
244
|
+
assert.equal(result.lastError, "temporary outage");
|
|
245
|
+
assert.equal(result.attempts, 0);
|
|
246
|
+
assert.equal(result.consecutiveFailures, 1);
|
|
247
|
+
assert.ok(Date.parse(result.runAt) > Date.now());
|
|
248
|
+
|
|
249
|
+
const persisted = await store.get("recurring-failure");
|
|
250
|
+
assert.equal(persisted.terminalFailure, undefined);
|
|
251
|
+
assert.equal(persisted.status, "pending");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("moves legacy Telegram routing out of task payloads", async () => {
|
|
255
|
+
await resetHome();
|
|
256
|
+
const store = new TaskStore();
|
|
257
|
+
const task = await store.add({
|
|
258
|
+
id: "routed",
|
|
259
|
+
kind: "agent_task",
|
|
260
|
+
payload: {
|
|
261
|
+
chatId: "owner",
|
|
262
|
+
telegramContext: { transportChatId: -1001, messageThreadId: 87 }
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.deepEqual(task.route, {
|
|
267
|
+
transport: "telegram",
|
|
268
|
+
destination: { chatId: -1001, threadId: 87 }
|
|
269
|
+
});
|
|
270
|
+
assert.equal(task.payload.telegramContext, undefined);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("records uncertain outcomes without retrying", async () => {
|
|
274
|
+
await resetHome();
|
|
275
|
+
const store = new TaskStore();
|
|
276
|
+
await store.add({ id: "uncertain", kind: "agent_task", status: "running", attempts: 1 });
|
|
277
|
+
|
|
278
|
+
const task = await store.retryOrFail("uncertain", "turn interrupted", {
|
|
279
|
+
retryable: false,
|
|
280
|
+
outcomeUncertain: true
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
assert.equal(task.status, "outcome_uncertain");
|
|
284
|
+
assert.equal(task.error, "turn interrupted");
|
|
285
|
+
assert.ok(task.uncertainAt);
|
|
286
|
+
});
|
|
287
|
+
|
|
117
288
|
test("fails and cancels tasks by id", async () => {
|
|
118
289
|
await resetHome();
|
|
119
290
|
const store = new TaskStore();
|
|
@@ -138,6 +309,7 @@ test("cancelAll preserves done and failed tasks and respects chat filters", asyn
|
|
|
138
309
|
await store.add({ id: "chat-2-pending", kind: "agent_task" }, { payload: { chatId: "chat-2" } });
|
|
139
310
|
await store.add({ id: "chat-1-done", kind: "agent_task", status: "done" }, { payload: { chatId: "chat-1" } });
|
|
140
311
|
await store.add({ id: "chat-1-failed", kind: "agent_task", status: "failed" }, { payload: { chatId: "chat-1" } });
|
|
312
|
+
await store.add({ id: "chat-1-uncertain", kind: "agent_task", status: "outcome_uncertain" }, { payload: { chatId: "chat-1" } });
|
|
141
313
|
|
|
142
314
|
const removed = await store.cancelAll({ chatId: "chat-1" });
|
|
143
315
|
const remaining = await store.list();
|
|
@@ -145,6 +317,6 @@ test("cancelAll preserves done and failed tasks and respects chat filters", asyn
|
|
|
145
317
|
assert.deepEqual(removed.map((task) => task.id), ["chat-1-pending"]);
|
|
146
318
|
assert.deepEqual(
|
|
147
319
|
remaining.map((task) => task.id),
|
|
148
|
-
["chat-2-pending", "chat-1-done", "chat-1-failed"]
|
|
320
|
+
["chat-2-pending", "chat-1-done", "chat-1-failed", "chat-1-uncertain"]
|
|
149
321
|
);
|
|
150
322
|
});
|
|
@@ -5,8 +5,12 @@ import { createTelegramTaskDispatcher } from "../src/transport/telegram/task-dis
|
|
|
5
5
|
function createHarness(overrides = {}) {
|
|
6
6
|
const calls = [];
|
|
7
7
|
const taskStore = {
|
|
8
|
-
async fail(...args) { calls.push(["fail", ...args]); },
|
|
9
|
-
async complete(...args) { calls.push(["complete", ...args]); },
|
|
8
|
+
async fail(...args) { calls.push(["fail", ...args]); return { status: "failed" }; },
|
|
9
|
+
async complete(...args) { calls.push(["complete", ...args]); return { status: "done" }; },
|
|
10
|
+
async retryOrFail(taskId, error, options) {
|
|
11
|
+
calls.push(["retryOrFail", taskId, error.message, options]);
|
|
12
|
+
return { status: options.retryable ? "pending" : "failed" };
|
|
13
|
+
},
|
|
10
14
|
async claimDue() { return []; },
|
|
11
15
|
...overrides.taskStore
|
|
12
16
|
};
|
|
@@ -17,31 +21,44 @@ function createHarness(overrides = {}) {
|
|
|
17
21
|
artifactStore: { forChat() { throw new Error("unexpected artifact access"); } },
|
|
18
22
|
toolRegistry: {},
|
|
19
23
|
resourceNotes: { async get() { return ""; } },
|
|
20
|
-
agentManager: { async runTool(input) { calls.push(["runTool", input]); } },
|
|
24
|
+
agentManager: { async runTool(input) { calls.push(["runTool", input]); return { ok: true }; } },
|
|
21
25
|
logger: null,
|
|
22
26
|
...overrides.dependencies
|
|
23
27
|
});
|
|
24
28
|
return { calls, taskStore, dispatcher };
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
test("
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
test("confirms an agent task only after prompt execution resolves", async () => {
|
|
32
|
+
let confirmExecution;
|
|
33
|
+
const execution = new Promise((resolve) => { confirmExecution = resolve; });
|
|
34
|
+
const { calls, dispatcher } = createHarness({
|
|
35
|
+
dependencies: {
|
|
36
|
+
enqueueAsyncPrompt: async (input) => {
|
|
37
|
+
calls.push(["enqueue", input]);
|
|
38
|
+
await execution;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const running = dispatcher.runClaimedTask({
|
|
30
43
|
id: "task-1",
|
|
31
44
|
kind: "agent_task",
|
|
32
|
-
|
|
45
|
+
status: "running",
|
|
46
|
+
payload: { chatId: 123, prompt: "do the thing" },
|
|
47
|
+
route: { transport: "telegram", destination: { chatId: -1001, threadId: 9 } }
|
|
33
48
|
});
|
|
34
49
|
|
|
50
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
35
51
|
assert.equal(calls[0][0], "enqueue");
|
|
36
|
-
assert.
|
|
37
|
-
assert.
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
assert.deepEqual(calls[0][1].route, { transport: "telegram", destination: { chatId: -1001, threadId: 9 } });
|
|
53
|
+
assert.equal(calls.some(([name]) => name === "complete"), false);
|
|
54
|
+
confirmExecution();
|
|
55
|
+
await running;
|
|
56
|
+
assert.deepEqual(calls.at(-1), ["complete", "task-1"]);
|
|
40
57
|
});
|
|
41
58
|
|
|
42
|
-
test("acknowledges an agent event before
|
|
59
|
+
test("acknowledges an agent event before executing it", async () => {
|
|
43
60
|
const { calls, dispatcher } = createHarness();
|
|
44
|
-
await dispatcher.
|
|
61
|
+
await dispatcher.runClaimedTask({
|
|
45
62
|
id: "event-1",
|
|
46
63
|
kind: "agent_event",
|
|
47
64
|
payload: { chatId: 123, prompt: "something happened", acknowledgement: "received" }
|
|
@@ -54,9 +71,9 @@ test("acknowledges an agent event before queueing it", async () => {
|
|
|
54
71
|
assert.deepEqual(calls[2], ["complete", "event-1"]);
|
|
55
72
|
});
|
|
56
73
|
|
|
57
|
-
test("runs poll tools headlessly and
|
|
74
|
+
test("runs poll tools headlessly and confirms their result", async () => {
|
|
58
75
|
const { calls, dispatcher } = createHarness();
|
|
59
|
-
await dispatcher.
|
|
76
|
+
await dispatcher.runClaimedTask({
|
|
60
77
|
id: "poll-1",
|
|
61
78
|
kind: "poll_tool",
|
|
62
79
|
payload: { chatId: 123, toolName: "checker", args: { cursor: "4" } }
|
|
@@ -68,18 +85,77 @@ test("runs poll tools headlessly and completes the checker task", async () => {
|
|
|
68
85
|
]);
|
|
69
86
|
});
|
|
70
87
|
|
|
71
|
-
test("
|
|
88
|
+
test("retries a known poll failure with backoff", async () => {
|
|
89
|
+
const { calls, dispatcher } = createHarness({
|
|
90
|
+
dependencies: {
|
|
91
|
+
agentManager: {
|
|
92
|
+
async runTool(input) {
|
|
93
|
+
calls.push(["runTool", input]);
|
|
94
|
+
return { ok: false, status: "failed", error: "temporary checker failure" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
await dispatcher.runClaimedTask({
|
|
100
|
+
id: "poll-failed",
|
|
101
|
+
kind: "poll_tool",
|
|
102
|
+
payload: { chatId: 123, toolName: "checker" }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(calls.at(-1), [
|
|
106
|
+
"retryOrFail",
|
|
107
|
+
"poll-failed",
|
|
108
|
+
"temporary checker failure",
|
|
109
|
+
{ retryable: true }
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("fails malformed tasks without retrying", async () => {
|
|
72
114
|
const { calls, dispatcher } = createHarness();
|
|
73
|
-
await dispatcher.
|
|
74
|
-
await dispatcher.
|
|
115
|
+
await dispatcher.runClaimedTask({ id: "bad-1", kind: "agent_task", payload: {} });
|
|
116
|
+
await dispatcher.runClaimedTask({ id: "bad-2", kind: "other", payload: { chatId: 123 } });
|
|
75
117
|
|
|
76
118
|
assert.deepEqual(calls, [
|
|
77
|
-
["
|
|
78
|
-
["
|
|
119
|
+
["retryOrFail", "bad-1", "Task missing chatId: agent_task", { retryable: false }],
|
|
120
|
+
["retryOrFail", "bad-2", "Unsupported task: other", { retryable: false }],
|
|
121
|
+
["send", 123, "⚠️ Arisa task failed\nTask: other (bad-2)\nError: Unsupported task: other\nNo further retries are scheduled.", undefined]
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("notifies the routed Telegram topic once retries are exhausted", async () => {
|
|
126
|
+
const { calls, dispatcher } = createHarness({
|
|
127
|
+
taskStore: {
|
|
128
|
+
async retryOrFail(taskId, error, options) {
|
|
129
|
+
calls.push(["retryOrFail", taskId, error.message, options]);
|
|
130
|
+
return {
|
|
131
|
+
status: "pending",
|
|
132
|
+
terminalFailure: true,
|
|
133
|
+
runAt: "2026-08-21T00:00:00.000Z"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
dependencies: {
|
|
138
|
+
enqueueAsyncPrompt: async () => { throw new Error("token=very-secret-value queue unavailable"); }
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await dispatcher.runClaimedTask({
|
|
143
|
+
id: "recurring-bad",
|
|
144
|
+
kind: "agent_task",
|
|
145
|
+
payload: { chatId: 123, prompt: "private payload must not appear" },
|
|
146
|
+
route: { transport: "telegram", destination: { chatId: -1001, threadId: 87 } }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.deepEqual(calls.at(-1), [
|
|
150
|
+
"send",
|
|
151
|
+
-1001,
|
|
152
|
+
"⚠️ Arisa task failed\nTask: agent_task (recurring-bad)\nError: token=[redacted] queue unavailable\nNext run: 2026-08-21T00:00:00.000Z",
|
|
153
|
+
{ message_thread_id: 87 }
|
|
79
154
|
]);
|
|
155
|
+
assert.equal(calls.at(-1)[2].includes("private payload"), false);
|
|
80
156
|
});
|
|
81
157
|
|
|
82
|
-
test("due-task dispatch
|
|
158
|
+
test("due-task dispatch retries one failure without blocking another task", async () => {
|
|
83
159
|
const tasks = [
|
|
84
160
|
{ id: "bad", kind: "agent_task", payload: { chatId: 123, prompt: "fail" } },
|
|
85
161
|
{ id: "good", kind: "poll_tool", payload: { chatId: 123, toolName: "checker" } }
|
|
@@ -95,8 +171,8 @@ test("due-task dispatch isolates failures between claimed tasks", async () => {
|
|
|
95
171
|
|
|
96
172
|
assert.deepEqual(calls, [
|
|
97
173
|
["claimDue", 10],
|
|
98
|
-
["fail", "bad", "queue unavailable"],
|
|
99
174
|
["runTool", { name: "checker", request: { args: {} }, chatId: 123 }],
|
|
100
|
-
["complete", "good"]
|
|
175
|
+
["complete", "good"],
|
|
176
|
+
["retryOrFail", "bad", "queue unavailable", { retryable: true }]
|
|
101
177
|
]);
|
|
102
178
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { buildPrompt, buildReactionPrompt, isScheduledTaskPrompt, shouldIncludeArtifactReference, withPromptSpeed } from "../src/transport/telegram/bot.js";
|
|
3
|
+
import { buildPrompt, buildReactionPrompt, isScheduledTaskPrompt, scheduledPromptSpeedOptions, shouldIncludeArtifactReference, withPromptSpeed } from "../src/transport/telegram/bot.js";
|
|
4
4
|
import { captureIncomingArtifact } from "../src/transport/telegram/media.js";
|
|
5
5
|
|
|
6
6
|
test("scheduled agent prompts use normal speed for one turn and restore chat speed", async () => {
|
|
@@ -11,7 +11,18 @@ test("scheduled agent prompts use normal speed for one turn and restore chat spe
|
|
|
11
11
|
assert.equal(isScheduledTaskPrompt("Scheduled task fired.\ntaskId: one"), true);
|
|
12
12
|
assert.equal(isScheduledTaskPrompt("Incoming Telegram message."), false);
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const speedOptions = scheduledPromptSpeedOptions({
|
|
15
|
+
prompt: "Scheduled task fired.\ntaskId: one",
|
|
16
|
+
session: {
|
|
17
|
+
model: { provider: "openai-codex", api: "openai-codex-responses", id: "gpt-5.5" }
|
|
18
|
+
},
|
|
19
|
+
speedController,
|
|
20
|
+
configuredSpeed: 1.5
|
|
21
|
+
});
|
|
22
|
+
assert.equal(speedOptions.speed, 1);
|
|
23
|
+
assert.equal(speedOptions.restoreSpeed(), 1.5);
|
|
24
|
+
|
|
25
|
+
await withPromptSpeed(speedOptions, async () => {
|
|
15
26
|
assert.equal(speed, 1);
|
|
16
27
|
});
|
|
17
28
|
assert.equal(speed, 1.5);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createTelegramToolsCommandHandler } from "../src/transport/telegram/telegram-tools-command.js";
|
|
4
|
+
|
|
5
|
+
test("/tools shows typing and sends the scoped usage report", async () => {
|
|
6
|
+
const events = [];
|
|
7
|
+
const handler = createTelegramToolsCommandHandler({
|
|
8
|
+
authorize: async () => ({ ok: true }),
|
|
9
|
+
contextRoute: () => ({ scopeChatId: 879964957 }),
|
|
10
|
+
toolRegistry: {
|
|
11
|
+
usage: async (chatId) => {
|
|
12
|
+
assert.equal(chatId, 879964957);
|
|
13
|
+
return [{ name: "example", count: 2, official: true }];
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
withTyping: async (_ctx, work) => {
|
|
17
|
+
events.push("typing");
|
|
18
|
+
return work();
|
|
19
|
+
},
|
|
20
|
+
logger: null
|
|
21
|
+
});
|
|
22
|
+
const ctx = {
|
|
23
|
+
chat: { id: 879964957 },
|
|
24
|
+
reply: async (text, options) => events.push({ text, options })
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await handler(ctx);
|
|
28
|
+
|
|
29
|
+
assert.equal(events[0], "typing");
|
|
30
|
+
assert.match(events[1].text, /example/);
|
|
31
|
+
assert.deepEqual(events[1].options, { parse_mode: "HTML" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("/tools reports failures instead of disappearing", async () => {
|
|
35
|
+
const replies = [];
|
|
36
|
+
const handler = createTelegramToolsCommandHandler({
|
|
37
|
+
authorize: async () => ({ ok: true }),
|
|
38
|
+
contextRoute: () => ({ scopeChatId: 1 }),
|
|
39
|
+
toolRegistry: { usage: async () => { throw new Error("store unavailable"); } },
|
|
40
|
+
withTyping: async (_ctx, work) => work(),
|
|
41
|
+
logger: null
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await handler({ chat: { id: 1 }, reply: async (text) => replies.push(text) });
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(replies, ["Tool usage report failed: store unavailable"]);
|
|
47
|
+
});
|