arisa 4.3.5 → 5.1.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.
- package/AGENTS.md +21 -19
- package/README.md +30 -9
- package/package.json +6 -2
- package/pnpm-workspace.yaml +1 -0
- package/src/core/agent/agent-manager.js +288 -29
- package/src/core/agent/auth-flow.js +12 -8
- package/src/core/agent/model-selection.js +54 -14
- package/src/core/agent/model-speed.js +59 -0
- package/src/core/config/config-defaults.js +56 -4
- package/src/core/config/config-store.js +5 -1
- package/src/core/conversation/conversation-history-store.js +142 -0
- package/src/core/tasks/task-store.js +16 -0
- package/src/core/tools/daemon-health.js +11 -2
- package/src/core/tools/daemon-processes.js +92 -2
- package/src/core/tools/daemon-runtime.js +4 -2
- package/src/core/tools/ipc-client.js +15 -3
- package/src/core/tools/tool-registry.js +42 -2
- package/src/core/tools/tool-usage-store.js +59 -0
- package/src/index.js +61 -6
- package/src/runtime/arisa-capabilities.js +45 -1
- package/src/runtime/bootstrap.js +3 -2
- package/src/runtime/create-app.js +49 -11
- package/src/runtime/doctor.js +365 -0
- package/src/runtime/log-viewer.js +165 -0
- package/src/runtime/paths.js +8 -1
- package/src/runtime/report-format.js +51 -0
- package/src/runtime/service-manager.js +106 -8
- package/src/runtime/tool-process-supervisor.js +107 -10
- package/src/runtime/tool-usage-report.js +10 -0
- package/src/runtime/update-manager.js +206 -0
- package/src/transport/telegram/bot.js +633 -91
- package/src/transport/telegram/model-picker.js +28 -2
- package/test/agent-tool-policy.test.js +26 -1
- package/test/auth-flow.test.js +28 -2
- package/test/capabilities-security.test.js +37 -0
- package/test/context-and-task-bounds.test.js +280 -0
- package/test/daemon-runtime.test.js +130 -2
- package/test/dependency-warnings.test.js +17 -0
- package/test/doctor.test.js +111 -0
- package/test/log-viewer.test.js +90 -0
- package/test/model-selection.test.js +125 -2
- package/test/paths.test.js +16 -0
- package/test/pi-compaction.test.js +43 -0
- package/test/service-manager.test.js +237 -0
- package/test/task-store.test.js +31 -0
- package/test/telegram-text-artifact.test.js +36 -1
- package/test/tool-registry-run.test.js +21 -0
- package/test/tool-usage.test.js +37 -0
- package/test/update-manager.test.js +87 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createTelegramRestartHandler, telegramCommands } from "../src/transport/telegram/bot.js";
|
|
4
|
+
import { handoffServiceRestart, restartService, serviceEntryFile, waitForServiceStop } from "../src/runtime/service-manager.js";
|
|
5
|
+
|
|
6
|
+
test("registers maintenance as native Telegram commands", () => {
|
|
7
|
+
assert.equal(
|
|
8
|
+
telegramCommands.some((command) => command.command === "restart"),
|
|
9
|
+
true
|
|
10
|
+
);
|
|
11
|
+
assert.equal(telegramCommands.some((command) => command.command === "update"), true);
|
|
12
|
+
assert.equal(telegramCommands.some((command) => command.command === "tools"), true);
|
|
13
|
+
assert.ok(telegramCommands.every((command) => command.description.length <= 24));
|
|
14
|
+
assert.equal(telegramCommands.some((command) => command.command === "harness"), false);
|
|
15
|
+
assert.equal(telegramCommands.some((command) => command.command === "login"), false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("replies before handing restart to a detached CLI process", async () => {
|
|
19
|
+
const calls = [];
|
|
20
|
+
let unreferenced = false;
|
|
21
|
+
let logClosed = false;
|
|
22
|
+
const environment = { ARISA_TEST: "restart" };
|
|
23
|
+
|
|
24
|
+
const result = await handoffServiceRestart({
|
|
25
|
+
verbose: false,
|
|
26
|
+
cliArgs: ["--pi.model", "example/model"]
|
|
27
|
+
}, {
|
|
28
|
+
ensureHome: async () => { calls.push("ensure-home"); },
|
|
29
|
+
getStatus: async () => {
|
|
30
|
+
calls.push("get-status");
|
|
31
|
+
return { running: true, pid: 41 };
|
|
32
|
+
},
|
|
33
|
+
openLog: async (file, mode) => {
|
|
34
|
+
calls.push(["open-log", file, mode]);
|
|
35
|
+
return {
|
|
36
|
+
fd: 17,
|
|
37
|
+
close: async () => { logClosed = true; }
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
spawnProcess: (command, args, options) => {
|
|
41
|
+
calls.push(["spawn", command, args, options]);
|
|
42
|
+
return {
|
|
43
|
+
pid: 84,
|
|
44
|
+
unref: () => { unreferenced = true; }
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
environment,
|
|
48
|
+
currentPid: 41
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(calls[0], "ensure-home");
|
|
52
|
+
assert.equal(calls[1], "get-status");
|
|
53
|
+
assert.equal(calls[2][0], "open-log");
|
|
54
|
+
assert.equal(calls[2][2], "a");
|
|
55
|
+
assert.equal(calls[3][0], "spawn");
|
|
56
|
+
assert.equal(calls[3][1], process.execPath);
|
|
57
|
+
assert.deepEqual(calls[3][2], [
|
|
58
|
+
serviceEntryFile,
|
|
59
|
+
"restart",
|
|
60
|
+
"--pi.model",
|
|
61
|
+
"example/model",
|
|
62
|
+
"--silent"
|
|
63
|
+
]);
|
|
64
|
+
assert.deepEqual(calls[3][3], {
|
|
65
|
+
detached: true,
|
|
66
|
+
stdio: ["ignore", 17, 17],
|
|
67
|
+
env: environment
|
|
68
|
+
});
|
|
69
|
+
assert.equal(unreferenced, true);
|
|
70
|
+
assert.equal(logClosed, true);
|
|
71
|
+
assert.equal(result.pid, 84);
|
|
72
|
+
|
|
73
|
+
const handlerCalls = [];
|
|
74
|
+
const handler = createTelegramRestartHandler({
|
|
75
|
+
authorize: async () => ({ ok: true }),
|
|
76
|
+
requestRestart: async () => {
|
|
77
|
+
handlerCalls.push("handoff");
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const ctx = {
|
|
82
|
+
reply: async (text) => { handlerCalls.push(["reply", text]); }
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await handler(ctx);
|
|
86
|
+
await handler(ctx);
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(handlerCalls, [
|
|
89
|
+
["reply", "Arisa is restarting. I'll be back shortly."],
|
|
90
|
+
"handoff",
|
|
91
|
+
["reply", "An Arisa restart is already in progress."]
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("refuses Telegram restart handoff outside the active background service", async () => {
|
|
96
|
+
let spawned = false;
|
|
97
|
+
|
|
98
|
+
await assert.rejects(
|
|
99
|
+
handoffServiceRestart({}, {
|
|
100
|
+
ensureHome: async () => {},
|
|
101
|
+
getStatus: async () => ({ running: true, pid: 99 }),
|
|
102
|
+
spawnProcess: () => {
|
|
103
|
+
spawned = true;
|
|
104
|
+
},
|
|
105
|
+
currentPid: 41
|
|
106
|
+
}),
|
|
107
|
+
/requires the active background service process/
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert.equal(spawned, false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("reports a failed Telegram restart handoff and permits retry", async () => {
|
|
114
|
+
const replies = [];
|
|
115
|
+
let attempts = 0;
|
|
116
|
+
const handler = createTelegramRestartHandler({
|
|
117
|
+
authorize: async () => ({ ok: true }),
|
|
118
|
+
requestRestart: async () => {
|
|
119
|
+
attempts += 1;
|
|
120
|
+
if (attempts === 1) throw new Error("synthetic handoff failure");
|
|
121
|
+
return { pid: 84 };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const ctx = { reply: async (text) => { replies.push(text); } };
|
|
125
|
+
|
|
126
|
+
await handler(ctx);
|
|
127
|
+
await handler(ctx);
|
|
128
|
+
|
|
129
|
+
assert.equal(attempts, 2);
|
|
130
|
+
assert.deepEqual(replies, [
|
|
131
|
+
"Arisa is restarting. I'll be back shortly.",
|
|
132
|
+
"Arisa could not be restarted: synthetic handoff failure",
|
|
133
|
+
"Arisa is restarting. I'll be back shortly."
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("waits until the stopped service releases its PID before restarting", async () => {
|
|
138
|
+
const calls = [];
|
|
139
|
+
const statuses = [
|
|
140
|
+
{ running: true, pid: 41 },
|
|
141
|
+
{ running: true, pid: 41 },
|
|
142
|
+
{ running: false, pid: null }
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const result = await restartService({
|
|
146
|
+
verbose: false,
|
|
147
|
+
cliArgs: ["--pi.model", "example/model"],
|
|
148
|
+
shutdownTimeoutMs: 1_000,
|
|
149
|
+
shutdownPollIntervalMs: 1
|
|
150
|
+
}, {
|
|
151
|
+
stop: async () => {
|
|
152
|
+
calls.push("stop");
|
|
153
|
+
return { ok: true, pid: 41 };
|
|
154
|
+
},
|
|
155
|
+
getStatus: async () => {
|
|
156
|
+
calls.push("status");
|
|
157
|
+
return statuses.shift();
|
|
158
|
+
},
|
|
159
|
+
start: async (options) => {
|
|
160
|
+
calls.push("start");
|
|
161
|
+
assert.deepEqual(options, {
|
|
162
|
+
verbose: false,
|
|
163
|
+
cliArgs: ["--pi.model", "example/model"]
|
|
164
|
+
});
|
|
165
|
+
return { ok: true, pid: 84, logFile: "/tmp/arisa.log" };
|
|
166
|
+
},
|
|
167
|
+
sleep: async () => {
|
|
168
|
+
calls.push("sleep");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
assert.deepEqual(calls, ["stop", "status", "sleep", "status", "sleep", "status", "start"]);
|
|
173
|
+
assert.deepEqual(result, {
|
|
174
|
+
ok: true,
|
|
175
|
+
pid: 84,
|
|
176
|
+
previousPid: 41,
|
|
177
|
+
wasRunning: true,
|
|
178
|
+
logFile: "/tmp/arisa.log"
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("restart starts Arisa when it is not running", async () => {
|
|
183
|
+
let statusChecks = 0;
|
|
184
|
+
const result = await restartService({
|
|
185
|
+
shutdownTimeoutMs: 1_000,
|
|
186
|
+
shutdownPollIntervalMs: 1
|
|
187
|
+
}, {
|
|
188
|
+
stop: async () => ({ ok: false, reason: "not-running", pid: null }),
|
|
189
|
+
getStatus: async () => {
|
|
190
|
+
statusChecks += 1;
|
|
191
|
+
return { running: false, pid: null };
|
|
192
|
+
},
|
|
193
|
+
start: async () => ({ ok: true, pid: 84, logFile: "/tmp/arisa.log" })
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
assert.equal(statusChecks, 0);
|
|
197
|
+
assert.deepEqual(result, {
|
|
198
|
+
ok: true,
|
|
199
|
+
pid: 84,
|
|
200
|
+
previousPid: null,
|
|
201
|
+
wasRunning: false,
|
|
202
|
+
logFile: "/tmp/arisa.log"
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("restart does not start a second service when shutdown times out", async () => {
|
|
207
|
+
let started = false;
|
|
208
|
+
|
|
209
|
+
await assert.rejects(
|
|
210
|
+
restartService({
|
|
211
|
+
shutdownTimeoutMs: 2,
|
|
212
|
+
shutdownPollIntervalMs: 1
|
|
213
|
+
}, {
|
|
214
|
+
stop: async () => ({ ok: true, pid: 41 }),
|
|
215
|
+
getStatus: async () => ({ running: true, pid: 41 }),
|
|
216
|
+
start: async () => {
|
|
217
|
+
started = true;
|
|
218
|
+
return { ok: true, pid: 84 };
|
|
219
|
+
},
|
|
220
|
+
sleep: async () => {}
|
|
221
|
+
}),
|
|
222
|
+
/did not stop within 2ms/
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
assert.equal(started, false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("requires explicit positive shutdown timing policy", async () => {
|
|
229
|
+
await assert.rejects(
|
|
230
|
+
waitForServiceStop({ timeoutMs: 0, pollIntervalMs: 1 }),
|
|
231
|
+
/positive shutdownTimeoutMs/
|
|
232
|
+
);
|
|
233
|
+
await assert.rejects(
|
|
234
|
+
waitForServiceStop({ timeoutMs: 1, pollIntervalMs: 0 }),
|
|
235
|
+
/positive shutdownPollIntervalMs/
|
|
236
|
+
);
|
|
237
|
+
});
|
package/test/task-store.test.js
CHANGED
|
@@ -59,6 +59,37 @@ test("claims only due pending tasks and marks them running", async () => {
|
|
|
59
59
|
assert.equal((await store.get("done")).status, "done");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test("recovers interrupted running tasks for retry after restart", async () => {
|
|
63
|
+
await resetHome();
|
|
64
|
+
const store = new TaskStore();
|
|
65
|
+
const runAt = new Date(Date.now() - 1000).toISOString();
|
|
66
|
+
|
|
67
|
+
await store.add({ id: "interrupted-once", kind: "agent_task", runAt, status: "running" });
|
|
68
|
+
await store.add({
|
|
69
|
+
id: "interrupted-recurring",
|
|
70
|
+
kind: "poll_tool",
|
|
71
|
+
runAt,
|
|
72
|
+
status: "running",
|
|
73
|
+
recurrence: { type: "interval", everySeconds: 60 }
|
|
74
|
+
});
|
|
75
|
+
await store.add({ id: "still-pending", kind: "agent_task", runAt });
|
|
76
|
+
await store.add({ id: "already-done", kind: "agent_task", runAt, status: "done" });
|
|
77
|
+
|
|
78
|
+
const recovered = await store.recoverInterrupted();
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(recovered.map((task) => task.id), ["interrupted-once", "interrupted-recurring"]);
|
|
81
|
+
assert.ok(recovered.every((task) => task.status === "pending"));
|
|
82
|
+
assert.ok(recovered.every((task) => task.runAt === runAt));
|
|
83
|
+
assert.equal((await store.get("still-pending")).status, "pending");
|
|
84
|
+
assert.equal((await store.get("already-done")).status, "done");
|
|
85
|
+
|
|
86
|
+
const restartedStore = new TaskStore();
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
(await restartedStore.claimDue()).map((task) => task.id),
|
|
89
|
+
["interrupted-once", "interrupted-recurring", "still-pending"]
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
62
93
|
test("completes one-off tasks and re-schedules recurring interval tasks", async () => {
|
|
63
94
|
await resetHome();
|
|
64
95
|
const store = new TaskStore();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { buildPrompt, shouldIncludeArtifactReference } from "../src/transport/telegram/bot.js";
|
|
3
|
+
import { buildPrompt, buildReactionPrompt, shouldIncludeArtifactReference } from "../src/transport/telegram/bot.js";
|
|
4
4
|
import { captureIncomingArtifact } from "../src/transport/telegram/media.js";
|
|
5
5
|
|
|
6
6
|
function createTextContext(text = "hello") {
|
|
@@ -60,6 +60,41 @@ test("keeps distinct artifacts visible to the prompt", () => {
|
|
|
60
60
|
);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("surfaces Telegram forwarding provenance in the prompt", () => {
|
|
64
|
+
const ctx = createTextContext("forwarded text");
|
|
65
|
+
ctx.message.forward_origin = {
|
|
66
|
+
type: "user",
|
|
67
|
+
sender_user: { id: 999, username: "source_user", first_name: "Source" },
|
|
68
|
+
date: 1_786_570_000
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const prompt = buildPrompt({ ctx });
|
|
72
|
+
|
|
73
|
+
assert.match(prompt, /forwarded: true/);
|
|
74
|
+
assert.match(prompt, /forwardedOriginType: user/);
|
|
75
|
+
assert.match(prompt, /forwardedFrom: @source_user/);
|
|
76
|
+
assert.match(prompt, /forwardedAt: 2026-/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("formats Telegram reaction changes as lightweight feedback", () => {
|
|
80
|
+
const prompt = buildReactionPrompt({
|
|
81
|
+
reaction: {
|
|
82
|
+
chat: { id: 123 },
|
|
83
|
+
user: { id: 456, username: "martin", first_name: "Martin" },
|
|
84
|
+
message_id: 321,
|
|
85
|
+
old_reaction: [{ type: "emoji", emoji: "👍" }],
|
|
86
|
+
new_reaction: [{ type: "emoji", emoji: "❤️" }]
|
|
87
|
+
},
|
|
88
|
+
reactedMessageText: "Updated draft intro"
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
assert.match(prompt, /reactedMessageId: 321/);
|
|
92
|
+
assert.match(prompt, /reactedMessageText: Updated draft intro/);
|
|
93
|
+
assert.match(prompt, /addedReactions: ❤️/);
|
|
94
|
+
assert.match(prompt, /removedReactions: 👍/);
|
|
95
|
+
assert.match(prompt, /otherwise stay silent/);
|
|
96
|
+
});
|
|
97
|
+
|
|
63
98
|
test("marks incoming Telegram text artifacts as internal inline messages", async () => {
|
|
64
99
|
const calls = [];
|
|
65
100
|
const artifactStore = {
|
|
@@ -155,12 +155,33 @@ test("runs a registered tool process with an enriched request and cleans up requ
|
|
|
155
155
|
});
|
|
156
156
|
assert.equal(result.output.env.ARISA_PACKAGE_DIR, arisaPackageDir);
|
|
157
157
|
assert.equal(result.output.env.ARISA_IPC_SOCKET, arisaIpcSocketFile);
|
|
158
|
+
assert.deepEqual(await registry.usage("chat-1"), [{ name: "fake-tool", count: 1 }]);
|
|
158
159
|
|
|
159
160
|
const requestFile = result.output.requestFile;
|
|
160
161
|
await assert.rejects(() => access(requestFile), { code: "ENOENT" });
|
|
161
162
|
await assert.rejects(() => access(path.dirname(requestFile)), { code: "ENOENT" });
|
|
162
163
|
});
|
|
163
164
|
|
|
165
|
+
test("keeps concurrent requests to the same tool isolated", async () => {
|
|
166
|
+
await resetHome();
|
|
167
|
+
await createFakeTool("fake-tool");
|
|
168
|
+
|
|
169
|
+
const registry = new ToolRegistry();
|
|
170
|
+
await registry.load();
|
|
171
|
+
|
|
172
|
+
const results = await Promise.all(Array.from({ length: 12 }, (_, index) => registry.run({
|
|
173
|
+
name: "fake-tool",
|
|
174
|
+
chatId: "chat-1",
|
|
175
|
+
request: { text: `request-${index}`, args: { index } }
|
|
176
|
+
})));
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(
|
|
179
|
+
results.map((result) => result.output.request.text).sort(),
|
|
180
|
+
Array.from({ length: 12 }, (_, index) => `request-${index}`).sort()
|
|
181
|
+
);
|
|
182
|
+
assert.equal(new Set(results.map((result) => result.output.requestFile)).size, 12);
|
|
183
|
+
});
|
|
184
|
+
|
|
164
185
|
test("rejects unknown tools", async () => {
|
|
165
186
|
await resetHome();
|
|
166
187
|
const registry = new ToolRegistry();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { ToolUsageStore } from "../src/core/tools/tool-usage-store.js";
|
|
7
|
+
import { formatToolUsageReport } from "../src/runtime/tool-usage-report.js";
|
|
8
|
+
|
|
9
|
+
test("counts concurrent tool uses per chat", async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "arisa-tool-usage-"));
|
|
11
|
+
const store = new ToolUsageStore({ resolveFile: (chatId) => path.join(root, String(chatId), "usage.json") });
|
|
12
|
+
try {
|
|
13
|
+
await Promise.all([
|
|
14
|
+
store.record("chat-1", "gmail-workspace"),
|
|
15
|
+
store.record("chat-1", "gmail-workspace"),
|
|
16
|
+
store.record("chat-1", "x-reader"),
|
|
17
|
+
store.record("chat-2", "gmail-workspace")
|
|
18
|
+
]);
|
|
19
|
+
assert.deepEqual(await store.counts("chat-1"), {
|
|
20
|
+
"gmail-workspace": 2,
|
|
21
|
+
"x-reader": 1
|
|
22
|
+
});
|
|
23
|
+
assert.deepEqual(await store.counts("chat-2"), { "gmail-workspace": 1 });
|
|
24
|
+
} finally {
|
|
25
|
+
await rm(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("formats narrow tool usage counts", () => {
|
|
30
|
+
const report = formatToolUsageReport([
|
|
31
|
+
{ name: "campaign-draft-runner", count: 12 },
|
|
32
|
+
{ name: "gmail-workspace", count: 3 }
|
|
33
|
+
]);
|
|
34
|
+
assert.match(report, /campaign-draft-runner\s+12/);
|
|
35
|
+
assert.match(report, /gmail-workspace\s+3/);
|
|
36
|
+
assert.ok(report.split("\n").every((line) => [...line].length <= 35));
|
|
37
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { compareVersions, formatUpdateReport } from "../src/runtime/update-manager.js";
|
|
4
|
+
|
|
5
|
+
test("compares semantic versions", () => {
|
|
6
|
+
assert.equal(compareVersions("5.0.2", "5.0.3"), -1);
|
|
7
|
+
assert.equal(compareVersions("5.1.0", "5.0.9"), 1);
|
|
8
|
+
assert.equal(compareVersions("5.0.2", "5.0.2"), 0);
|
|
9
|
+
assert.equal(compareVersions("invalid", "5.0.2"), null);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("formats core and official tool update status", () => {
|
|
13
|
+
assert.equal(formatUpdateReport({
|
|
14
|
+
core: { currentVersion: "5.0.2", latestVersion: "5.1.0", updateAvailable: true },
|
|
15
|
+
bootstrapInstalled: ["official-tool-sync"],
|
|
16
|
+
tools: {
|
|
17
|
+
installedOfficial: 3,
|
|
18
|
+
official: [
|
|
19
|
+
{ name: "context-vault", status: "up-to-date" },
|
|
20
|
+
{ name: "customized", status: "diverged" },
|
|
21
|
+
{ name: "gmail-workspace", status: "upstream-update" }
|
|
22
|
+
],
|
|
23
|
+
nonOfficial: ["private-helper"],
|
|
24
|
+
counts: { "up-to-date": 1, "upstream-update": 1, diverged: 1 },
|
|
25
|
+
updateable: ["context-vault"],
|
|
26
|
+
blocked: [{ name: "customized", status: "diverged" }]
|
|
27
|
+
}
|
|
28
|
+
}), [
|
|
29
|
+
"```text",
|
|
30
|
+
"Arisa update",
|
|
31
|
+
"============",
|
|
32
|
+
"Core",
|
|
33
|
+
" Current 5.0.2",
|
|
34
|
+
" Latest 5.1.0",
|
|
35
|
+
" Status update available",
|
|
36
|
+
"",
|
|
37
|
+
"Official tools",
|
|
38
|
+
" Installed 3",
|
|
39
|
+
" up-to-date 1",
|
|
40
|
+
" upstream-update 1",
|
|
41
|
+
" diverged 1",
|
|
42
|
+
"",
|
|
43
|
+
"Official (3)",
|
|
44
|
+
" - context-vault",
|
|
45
|
+
" - customized [diverged]",
|
|
46
|
+
" - gmail-workspace",
|
|
47
|
+
" [upstream-update]",
|
|
48
|
+
"",
|
|
49
|
+
"Non-official (1)",
|
|
50
|
+
" - private-helper",
|
|
51
|
+
"",
|
|
52
|
+
"Safe updates",
|
|
53
|
+
" - context-vault",
|
|
54
|
+
"",
|
|
55
|
+
"Needs review",
|
|
56
|
+
" - customized",
|
|
57
|
+
" [diverged]",
|
|
58
|
+
"",
|
|
59
|
+
"Update support installed",
|
|
60
|
+
" - official-tool-sync",
|
|
61
|
+
"```"
|
|
62
|
+
].join("\n"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("shortens long review status labels", () => {
|
|
66
|
+
const report = formatUpdateReport({
|
|
67
|
+
core: { currentVersion: "5.0.2", latestVersion: "5.0.2", updateAvailable: false },
|
|
68
|
+
bootstrapInstalled: [],
|
|
69
|
+
tools: {
|
|
70
|
+
installedOfficial: 2,
|
|
71
|
+
official: [
|
|
72
|
+
{ name: "audio-extractor", status: "locally-modified" },
|
|
73
|
+
{ name: "campaign-draft-runner", status: "untracked-difference" }
|
|
74
|
+
],
|
|
75
|
+
nonOfficial: [],
|
|
76
|
+
counts: {},
|
|
77
|
+
updateable: [],
|
|
78
|
+
blocked: [
|
|
79
|
+
{ name: "audio-extractor", status: "locally-modified" },
|
|
80
|
+
{ name: "campaign-draft-runner", status: "untracked-difference" }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
assert.match(report, /audio-extractor\n \[local\]/);
|
|
85
|
+
assert.match(report, /campaign-draft-runner\n \[untracked\]/);
|
|
86
|
+
assert.ok(report.split("\n").every((line) => [...line].length <= 35));
|
|
87
|
+
});
|