arisa 5.1.10 → 5.1.12
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/package.json +8 -7
- package/src/runtime/create-app.js +4 -2
- package/src/runtime/slave-cli.js +1 -13
- package/src/runtime/slave-service.js +5 -20
- package/src/runtime/update-manager.js +43 -0
- package/src/transport/telegram/bot.js +21 -4
- package/src/transport/telegram/update-command.js +122 -0
- package/test/slave-cli.test.js +23 -11
- package/test/update-command.test.js +139 -0
- package/test/update-manager.test.js +57 -1
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.12",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"arisa": "bin/arisa.js"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"bootstrap": "node src/index.js --bootstrap",
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"telegram",
|
|
12
17
|
"pi-agent",
|
|
@@ -39,15 +44,11 @@
|
|
|
39
44
|
"url": "https://github.com/clasen/Arisa/issues"
|
|
40
45
|
},
|
|
41
46
|
"homepage": "https://arisa.sh",
|
|
47
|
+
"packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215",
|
|
42
48
|
"dependencies": {
|
|
43
49
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
44
50
|
"@sinclair/typebox": "^0.34.41",
|
|
45
51
|
"grammy": "^1.42.0",
|
|
46
52
|
"typebox": "1.1.38"
|
|
47
|
-
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"start": "node src/index.js",
|
|
50
|
-
"bootstrap": "node src/index.js --bootstrap",
|
|
51
|
-
"test": "node --test"
|
|
52
53
|
}
|
|
53
|
-
}
|
|
54
|
+
}
|
|
@@ -11,7 +11,7 @@ import { createIpcServer } from "./ipc/ipc-server.js";
|
|
|
11
11
|
import { getAgentConfig } from "../core/agent/model-selection.js";
|
|
12
12
|
import { normalizeModelSpeed } from "../core/agent/model-speed.js";
|
|
13
13
|
import { runDoctor } from "./doctor.js";
|
|
14
|
-
import { checkForUpdates,
|
|
14
|
+
import { checkForUpdates, installCoreUpdate, updateOfficialTools } from "./update-manager.js";
|
|
15
15
|
|
|
16
16
|
function normalizeString(value) {
|
|
17
17
|
const text = String(value ?? "").trim();
|
|
@@ -146,7 +146,9 @@ export async function createApp({ logger, runtimeOverrides, requestRestart } = {
|
|
|
146
146
|
},
|
|
147
147
|
logger
|
|
148
148
|
}),
|
|
149
|
-
checkUpdates: async (chatId) =>
|
|
149
|
+
checkUpdates: async (chatId) => checkForUpdates({ chatId, toolRegistry }),
|
|
150
|
+
updateCore: async (targetVersion) => installCoreUpdate({ targetVersion }),
|
|
151
|
+
updateTools: async (chatId) => updateOfficialTools({ chatId, toolRegistry }),
|
|
150
152
|
requestRestart,
|
|
151
153
|
logger
|
|
152
154
|
});
|
package/src/runtime/slave-cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
|
-
import { stdin, stdout } from "node:process";
|
|
6
4
|
import { applyConfigDefaults } from "../core/config/config-defaults.js";
|
|
7
5
|
import { installLockedOfficialTool } from "../core/tools/official-tool-installer.js";
|
|
8
6
|
import { createHeadlessApp } from "./create-headless-app.js";
|
|
@@ -100,15 +98,6 @@ export async function invokeSlaveTool(paths, args, { run = runProcess } = {}) {
|
|
|
100
98
|
});
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
async function askFromTerminal(prompt) {
|
|
104
|
-
const terminal = readline.createInterface({ input: stdin, output: stdout });
|
|
105
|
-
try {
|
|
106
|
-
return await terminal.question(`${prompt}: `);
|
|
107
|
-
} finally {
|
|
108
|
-
terminal.close();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
101
|
async function listSlaveTools(paths) {
|
|
113
102
|
const entries = await readdir(paths.toolsDir, { withFileTypes: true }).catch(() => []);
|
|
114
103
|
const tools = [];
|
|
@@ -175,7 +164,6 @@ function explainSlaveBootstrapError(error) {
|
|
|
175
164
|
|
|
176
165
|
export async function runSlaveBootstrap(url, {
|
|
177
166
|
paths = getSlavePaths(resolveSlaveHome()),
|
|
178
|
-
ask = askFromTerminal,
|
|
179
167
|
selectAccount = selectSlaveServiceAccount,
|
|
180
168
|
ensureTool = ensureMasterSlaveTool,
|
|
181
169
|
installService = installSlaveSystemdService,
|
|
@@ -186,7 +174,7 @@ export async function runSlaveBootstrap(url, {
|
|
|
186
174
|
} = {}) {
|
|
187
175
|
parseSlaveBootstrapUrl(url);
|
|
188
176
|
if (platform !== "linux") throw new Error("Arisa Slave service installation currently requires Linux with systemd");
|
|
189
|
-
const account = await selectAccount(
|
|
177
|
+
const account = await selectAccount();
|
|
190
178
|
await ensureSlaveConfig(paths);
|
|
191
179
|
await ensureTool(paths);
|
|
192
180
|
let result;
|
|
@@ -39,29 +39,13 @@ export function getSlavePaths(slaveHome) {
|
|
|
39
39
|
export async function selectSlaveServiceAccount({
|
|
40
40
|
euid = process.geteuid?.(),
|
|
41
41
|
currentUser = os.userInfo().username,
|
|
42
|
-
|
|
42
|
+
environment = process.env
|
|
43
43
|
} = {}) {
|
|
44
44
|
if (euid !== 0) {
|
|
45
45
|
return { scope: "user", user: requireAccountName(currentUser), root: false, dedicated: false };
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"Run Arisa Slave as:",
|
|
50
|
-
"1. dedicated user arisa-slave (recommended)",
|
|
51
|
-
"2. another existing user",
|
|
52
|
-
"3. root",
|
|
53
|
-
"Selection"
|
|
54
|
-
].join("\n"))).trim();
|
|
55
|
-
if (choice === "1") return { scope: "system", user: "arisa-slave", root: false, dedicated: true };
|
|
56
|
-
if (choice === "2") {
|
|
57
|
-
return { scope: "system", user: requireAccountName(await ask("Existing service user")), root: false, dedicated: false };
|
|
58
|
-
}
|
|
59
|
-
if (choice === "3") {
|
|
60
|
-
const confirmation = String(await ask("Type RUN AS ROOT to confirm full root authority")).trim();
|
|
61
|
-
if (confirmation !== "RUN AS ROOT") throw new Error("Root execution was not confirmed");
|
|
62
|
-
return { scope: "system", user: "root", root: true, dedicated: false };
|
|
63
|
-
}
|
|
64
|
-
throw new Error("Invalid Arisa Slave service account selection");
|
|
47
|
+
const user = requireAccountName(environment.SUDO_USER || currentUser);
|
|
48
|
+
return { scope: "system", user, root: user === "root", dedicated: false };
|
|
65
49
|
}
|
|
66
50
|
|
|
67
51
|
function quoteSystemd(value) {
|
|
@@ -166,7 +150,8 @@ export async function installSlaveSystemdService({
|
|
|
166
150
|
}
|
|
167
151
|
const systemctlArgs = account.scope === "user" ? ["--user"] : [];
|
|
168
152
|
await execute("systemctl", [...systemctlArgs, "daemon-reload"], { env: environment });
|
|
169
|
-
await execute("systemctl", [...systemctlArgs, "enable",
|
|
153
|
+
await execute("systemctl", [...systemctlArgs, "enable", slaveServiceName], { env: environment });
|
|
154
|
+
await execute("systemctl", [...systemctlArgs, "restart", slaveServiceName], { env: environment });
|
|
170
155
|
return { unitFile, serviceName: slaveServiceName, account, paths };
|
|
171
156
|
}
|
|
172
157
|
|
|
@@ -58,6 +58,32 @@ async function fetchLatestVersion() {
|
|
|
58
58
|
return version;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export async function installCoreUpdate({ targetVersion }, {
|
|
62
|
+
readVersion = readCurrentVersion,
|
|
63
|
+
fetchVersion = fetchLatestVersion,
|
|
64
|
+
execute = runCommand
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (!parseSemver(targetVersion)) throw new Error("Invalid Arisa update version");
|
|
67
|
+
const [currentVersion, latestVersion] = await Promise.all([readVersion(), fetchVersion()]);
|
|
68
|
+
if (targetVersion !== latestVersion) {
|
|
69
|
+
throw new Error(`Arisa ${latestVersion} is now the latest version. Run /update again.`);
|
|
70
|
+
}
|
|
71
|
+
if (compareVersions(currentVersion, latestVersion) !== -1) {
|
|
72
|
+
return { updated: false, currentVersion, latestVersion };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await execute("npm", ["install", "--global", `arisa@${latestVersion}`]);
|
|
76
|
+
const installedVersion = await readVersion();
|
|
77
|
+
if (installedVersion !== latestVersion) {
|
|
78
|
+
throw new Error(`npm completed, but Arisa is still ${installedVersion} instead of ${latestVersion}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
updated: true,
|
|
82
|
+
previousVersion: currentVersion,
|
|
83
|
+
currentVersion: installedVersion
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
async function cloneCatalog(scratchRoot) {
|
|
62
88
|
const repoDir = path.join(scratchRoot, "repo");
|
|
63
89
|
await runCommand("git", ["clone", "--depth", "1", "--branch", defaultBranch, "--", defaultRepoUrl, repoDir], { cwd: scratchRoot, timeoutMs: 180_000 });
|
|
@@ -156,6 +182,23 @@ export async function checkForUpdates({ chatId, toolRegistry }) {
|
|
|
156
182
|
};
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
export async function updateOfficialTools({ chatId, toolRegistry }) {
|
|
186
|
+
try {
|
|
187
|
+
const result = await toolRegistry.run({
|
|
188
|
+
name: "official-tool-sync",
|
|
189
|
+
chatId,
|
|
190
|
+
request: { args: { action: "update-safe" } }
|
|
191
|
+
});
|
|
192
|
+
const sync = parseToolSyncOutput(result);
|
|
193
|
+
return {
|
|
194
|
+
updated: (sync.updates || []).filter((item) => item.action === "updated").map((item) => item.name),
|
|
195
|
+
skipped: (sync.skipped || []).map(({ name, status }) => ({ name, status }))
|
|
196
|
+
};
|
|
197
|
+
} finally {
|
|
198
|
+
await toolRegistry.load();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
function shortToolStatus(status) {
|
|
160
203
|
return ({
|
|
161
204
|
"locally-modified": "local",
|
|
@@ -16,6 +16,8 @@ import { ConversationHistoryStore } from "../../core/conversation/conversation-h
|
|
|
16
16
|
import { formatDoctorReport } from "../../runtime/doctor.js";
|
|
17
17
|
import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
|
|
18
18
|
import { ToolResourceNoteStore } from "../../core/tools/tool-resource-note-store.js";
|
|
19
|
+
import { formatUpdateReport } from "../../runtime/update-manager.js";
|
|
20
|
+
import { buildUpdatePicker, createTelegramUpdateCallbackHandler } from "./update-command.js";
|
|
19
21
|
|
|
20
22
|
const slowPromptNoticeMs = 300_000;
|
|
21
23
|
|
|
@@ -23,7 +25,7 @@ export const telegramCommands = Object.freeze([
|
|
|
23
25
|
{ command: "new", description: "New chat context" },
|
|
24
26
|
{ command: "restart", description: "Restart Arisa" },
|
|
25
27
|
{ command: "doctor", description: "Check runtime health" },
|
|
26
|
-
{ command: "update", description: "Check
|
|
28
|
+
{ command: "update", description: "Check and apply updates" },
|
|
27
29
|
{ command: "tools", description: "Tool usage counts" },
|
|
28
30
|
{ command: "model", description: "Choose chat model" },
|
|
29
31
|
{ command: "effort", description: "Choose reasoning effort" },
|
|
@@ -568,7 +570,7 @@ export async function closeModelPicker(ctx, { messageText, callbackText }) {
|
|
|
568
570
|
await ctx.answerCallbackQuery({ text: callbackText });
|
|
569
571
|
}
|
|
570
572
|
|
|
571
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, requestRestart, logger }) {
|
|
573
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, updateCore, updateTools, requestRestart, logger }) {
|
|
572
574
|
const resourceNotes = new ToolResourceNoteStore();
|
|
573
575
|
const bot = new Bot(config.telegram.token);
|
|
574
576
|
const perChatState = createChatStateStore();
|
|
@@ -588,6 +590,18 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
588
590
|
requestRestart,
|
|
589
591
|
logger
|
|
590
592
|
});
|
|
593
|
+
const handleUpdateCallback = createTelegramUpdateCallbackHandler({
|
|
594
|
+
authorize: (ctx) => authorizeChat({
|
|
595
|
+
config,
|
|
596
|
+
chatId: ctx.chat.id,
|
|
597
|
+
saveConfig,
|
|
598
|
+
chatMeta: getIncomingChatMeta(ctx)
|
|
599
|
+
}),
|
|
600
|
+
updateCore,
|
|
601
|
+
updateTools,
|
|
602
|
+
requestRestart,
|
|
603
|
+
logger
|
|
604
|
+
});
|
|
591
605
|
|
|
592
606
|
function chatKey(chatId) {
|
|
593
607
|
return String(chatId);
|
|
@@ -1272,11 +1286,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1272
1286
|
if (!auth.ok) return;
|
|
1273
1287
|
const pending = await ctx.reply(renderTelegramHtml("```text\nChecking Arisa and official tool updates…\n```"), { parse_mode: "HTML" });
|
|
1274
1288
|
try {
|
|
1289
|
+
const report = await checkUpdates(ctx.chat.id);
|
|
1290
|
+
const picker = buildUpdatePicker(report);
|
|
1275
1291
|
await ctx.api.editMessageText(
|
|
1276
1292
|
ctx.chat.id,
|
|
1277
1293
|
pending.message_id,
|
|
1278
|
-
renderTelegramHtml(
|
|
1279
|
-
{ parse_mode: "HTML" }
|
|
1294
|
+
renderTelegramHtml(formatUpdateReport(report)),
|
|
1295
|
+
{ parse_mode: "HTML", reply_markup: picker.replyMarkup }
|
|
1280
1296
|
);
|
|
1281
1297
|
} catch (error) {
|
|
1282
1298
|
logger?.error("update", `update check failed: ${getErrorMessage(error)}`);
|
|
@@ -1342,6 +1358,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1342
1358
|
});
|
|
1343
1359
|
|
|
1344
1360
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
1361
|
+
if (await handleUpdateCallback(ctx)) return;
|
|
1345
1362
|
const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
|
|
1346
1363
|
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
1347
1364
|
const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const callbackPrefix = "arisa-update:";
|
|
2
|
+
|
|
3
|
+
function callbackMessage(ctx) {
|
|
4
|
+
return {
|
|
5
|
+
chatId: ctx.chat.id,
|
|
6
|
+
messageId: ctx.callbackQuery.message.message_id
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function editCallbackMessage(ctx, text, extra) {
|
|
11
|
+
const { chatId, messageId } = callbackMessage(ctx);
|
|
12
|
+
return extra
|
|
13
|
+
? ctx.api.editMessageText(chatId, messageId, text, extra)
|
|
14
|
+
: ctx.api.editMessageText(chatId, messageId, text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildUpdatePicker(report) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
if (report.core.updateAvailable) {
|
|
20
|
+
rows.push([{
|
|
21
|
+
text: `Update Arisa → ${report.core.latestVersion}`,
|
|
22
|
+
callback_data: `${callbackPrefix}core:${report.core.latestVersion}`
|
|
23
|
+
}]);
|
|
24
|
+
}
|
|
25
|
+
if (report.tools.updateable.length) {
|
|
26
|
+
rows.push([{
|
|
27
|
+
text: `Update safe tools (${report.tools.updateable.length})`,
|
|
28
|
+
callback_data: `${callbackPrefix}tools`
|
|
29
|
+
}]);
|
|
30
|
+
}
|
|
31
|
+
rows.push(rows.length
|
|
32
|
+
? [{ text: "Not now", callback_data: `${callbackPrefix}close` }]
|
|
33
|
+
: [{ text: "Up to date", callback_data: `${callbackPrefix}noop` }]);
|
|
34
|
+
return { replyMarkup: { inline_keyboard: rows } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseUpdateAction(data) {
|
|
38
|
+
if (data === `${callbackPrefix}tools`) return { type: "tools" };
|
|
39
|
+
if (data === `${callbackPrefix}close`) return { type: "close" };
|
|
40
|
+
if (data === `${callbackPrefix}noop`) return { type: "noop" };
|
|
41
|
+
const core = String(data || "").match(/^arisa-update:core:(\d+\.\d+\.\d+)$/);
|
|
42
|
+
return core ? { type: "core", targetVersion: core[1] } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatOfficialToolUpdateResult(result) {
|
|
46
|
+
const lines = result.updated.length
|
|
47
|
+
? ["Updated official tools:", ...result.updated.map((name) => `- ${name}`)]
|
|
48
|
+
: ["Official tools were already up to date."];
|
|
49
|
+
if (result.skipped.length) {
|
|
50
|
+
lines.push("", "Not changed (needs review):");
|
|
51
|
+
for (const item of result.skipped) lines.push(`- ${item.name} [${item.status}]`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createTelegramUpdateCallbackHandler({ authorize, updateCore, updateTools, requestRestart, logger }) {
|
|
57
|
+
let updateInProgress = false;
|
|
58
|
+
|
|
59
|
+
return async function handleTelegramUpdateCallback(ctx) {
|
|
60
|
+
const action = parseUpdateAction(ctx.callbackQuery.data);
|
|
61
|
+
if (!action) return false;
|
|
62
|
+
|
|
63
|
+
const auth = await authorize(ctx);
|
|
64
|
+
if (!auth.ok) {
|
|
65
|
+
await ctx.answerCallbackQuery({ text: "This chat is not authorized.", show_alert: true });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (action.type === "noop") {
|
|
69
|
+
await ctx.answerCallbackQuery({ text: "Arisa and official tools are up to date." });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (action.type === "close") {
|
|
73
|
+
await ctx.answerCallbackQuery();
|
|
74
|
+
await editCallbackMessage(ctx, "Update cancelled.");
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (updateInProgress) {
|
|
78
|
+
await ctx.answerCallbackQuery({ text: "An update is already in progress.", show_alert: true });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateInProgress = true;
|
|
83
|
+
const coreUpdate = action.type === "core";
|
|
84
|
+
try {
|
|
85
|
+
await ctx.answerCallbackQuery({ text: coreUpdate ? "Updating Arisa…" : "Updating safe tools…" });
|
|
86
|
+
if (coreUpdate) {
|
|
87
|
+
await editCallbackMessage(ctx, `Updating Arisa to ${action.targetVersion}…`);
|
|
88
|
+
const result = await updateCore(action.targetVersion);
|
|
89
|
+
if (!result.updated) {
|
|
90
|
+
await editCallbackMessage(ctx, `Arisa ${result.currentVersion} is already up to date.`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
await editCallbackMessage(
|
|
94
|
+
ctx,
|
|
95
|
+
`Arisa updated from ${result.previousVersion} to ${result.currentVersion}. Restarting…`
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
await requestRestart();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
logger?.error("update", `restart after update failed: ${message}`);
|
|
102
|
+
await editCallbackMessage(
|
|
103
|
+
ctx,
|
|
104
|
+
`Arisa updated to ${result.currentVersion}, but restart failed: ${message}\nRun /restart to activate it.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await editCallbackMessage(ctx, "Updating safe official tools…");
|
|
111
|
+
await editCallbackMessage(ctx, formatOfficialToolUpdateResult(await updateTools(ctx.chat.id)));
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
logger?.error("update", `update action failed: ${message}`);
|
|
116
|
+
await editCallbackMessage(ctx, `Arisa update failed: ${message}`).catch(() => {});
|
|
117
|
+
return true;
|
|
118
|
+
} finally {
|
|
119
|
+
updateInProgress = false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
package/test/slave-cli.test.js
CHANGED
|
@@ -58,16 +58,21 @@ test("hands one-shot requests through a 0600 file and removes it", async (t) =>
|
|
|
58
58
|
await assert.rejects(() => access(handedFile), { code: "ENOENT" });
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test("
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
() => selectSlaveServiceAccount({ euid: 0, ask: async () => rejectedAnswers.shift() }),
|
|
66
|
-
/Root execution was not confirmed/
|
|
61
|
+
test("selects the invoking service account without prompting", async () => {
|
|
62
|
+
assert.deepEqual(
|
|
63
|
+
await selectSlaveServiceAccount({ euid: 1000, currentUser: "storybot", environment: {} }),
|
|
64
|
+
{ scope: "user", user: "storybot", root: false, dedicated: false }
|
|
67
65
|
);
|
|
68
|
-
const acceptedAnswers = ["3", "RUN AS ROOT"];
|
|
69
66
|
assert.deepEqual(
|
|
70
|
-
await selectSlaveServiceAccount({
|
|
67
|
+
await selectSlaveServiceAccount({
|
|
68
|
+
euid: 0,
|
|
69
|
+
currentUser: "root",
|
|
70
|
+
environment: { SUDO_USER: "storybot" }
|
|
71
|
+
}),
|
|
72
|
+
{ scope: "system", user: "storybot", root: false, dedicated: false }
|
|
73
|
+
);
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
await selectSlaveServiceAccount({ euid: 0, currentUser: "root", environment: {} }),
|
|
71
76
|
{ scope: "system", user: "root", root: true, dedicated: false }
|
|
72
77
|
);
|
|
73
78
|
});
|
|
@@ -109,7 +114,7 @@ test("refuses to replace the PID of an active Slave host", async (t) => {
|
|
|
109
114
|
await assert.rejects(() => registerSlaveServiceProcess(paths), /already running/);
|
|
110
115
|
});
|
|
111
116
|
|
|
112
|
-
test("installs the
|
|
117
|
+
test("installs and restarts the Linux systemd target after pairing", async (t) => {
|
|
113
118
|
const root = await mkdtemp(path.join(os.tmpdir(), "arisa-slave-systemd-"));
|
|
114
119
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
115
120
|
const calls = [];
|
|
@@ -132,7 +137,11 @@ test("installs the dedicated Linux systemd target without silently selecting roo
|
|
|
132
137
|
assert.equal(result.account.root, false);
|
|
133
138
|
assert.equal(await access(result.unitFile).then(() => true, () => false), true);
|
|
134
139
|
assert.ok(calls.some(([command]) => command === "useradd"));
|
|
135
|
-
assert.
|
|
140
|
+
assert.deepEqual(calls.filter(([command]) => command === "systemctl"), [
|
|
141
|
+
["systemctl", ["daemon-reload"]],
|
|
142
|
+
["systemctl", ["enable", "arisa-slave.service"]],
|
|
143
|
+
["systemctl", ["restart", "arisa-slave.service"]]
|
|
144
|
+
]);
|
|
136
145
|
});
|
|
137
146
|
|
|
138
147
|
test("validates before effects and keeps the bootstrap secret out of service metadata", async (t) => {
|
|
@@ -144,7 +153,10 @@ test("validates before effects and keeps the bootstrap secret out of service met
|
|
|
144
153
|
paths,
|
|
145
154
|
entryFile: "/opt/arisa/src/index.js",
|
|
146
155
|
platform: "linux",
|
|
147
|
-
selectAccount: async () =>
|
|
156
|
+
selectAccount: async (...args) => {
|
|
157
|
+
assert.deepEqual(args, []);
|
|
158
|
+
return { scope: "user", user: "tester", root: false, dedicated: false };
|
|
159
|
+
},
|
|
148
160
|
ensureTool: async () => { calls.push("tool"); },
|
|
149
161
|
installService: async () => { calls.push("service"); },
|
|
150
162
|
invokeTool: async (_paths, args) => {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildUpdatePicker,
|
|
5
|
+
createTelegramUpdateCallbackHandler,
|
|
6
|
+
parseUpdateAction
|
|
7
|
+
} from "../src/transport/telegram/update-command.js";
|
|
8
|
+
|
|
9
|
+
function report(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
core: { currentVersion: "5.0.2", latestVersion: "5.1.0", updateAvailable: true },
|
|
12
|
+
bootstrapInstalled: [],
|
|
13
|
+
tools: {
|
|
14
|
+
installedOfficial: 2,
|
|
15
|
+
official: [],
|
|
16
|
+
nonOfficial: [],
|
|
17
|
+
counts: {},
|
|
18
|
+
updateable: ["context-vault", "gmail-workspace"],
|
|
19
|
+
blocked: []
|
|
20
|
+
},
|
|
21
|
+
...overrides
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("offers only available update actions", () => {
|
|
26
|
+
assert.deepEqual(buildUpdatePicker(report()).replyMarkup.inline_keyboard, [
|
|
27
|
+
[{ text: "Update Arisa → 5.1.0", callback_data: "arisa-update:core:5.1.0" }],
|
|
28
|
+
[{ text: "Update safe tools (2)", callback_data: "arisa-update:tools" }],
|
|
29
|
+
[{ text: "Not now", callback_data: "arisa-update:close" }]
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(buildUpdatePicker(report({
|
|
33
|
+
core: { currentVersion: "5.1.0", latestVersion: "5.1.0", updateAvailable: false },
|
|
34
|
+
tools: { ...report().tools, updateable: [] }
|
|
35
|
+
})).replyMarkup.inline_keyboard, [
|
|
36
|
+
[{ text: "Up to date", callback_data: "arisa-update:noop" }]
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("parses only valid update callback data", () => {
|
|
41
|
+
assert.deepEqual(parseUpdateAction("arisa-update:core:5.1.0"), { type: "core", targetVersion: "5.1.0" });
|
|
42
|
+
assert.deepEqual(parseUpdateAction("arisa-update:tools"), { type: "tools" });
|
|
43
|
+
assert.deepEqual(parseUpdateAction("arisa-update:close"), { type: "close" });
|
|
44
|
+
assert.deepEqual(parseUpdateAction("arisa-update:noop"), { type: "noop" });
|
|
45
|
+
assert.equal(parseUpdateAction("arisa-update:core:latest"), null);
|
|
46
|
+
assert.equal(parseUpdateAction("model:1"), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("applies a confirmed core update and then requests restart", async () => {
|
|
50
|
+
const calls = [];
|
|
51
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
52
|
+
authorize: async () => ({ ok: true }),
|
|
53
|
+
updateCore: async (targetVersion) => {
|
|
54
|
+
calls.push(["updateCore", targetVersion]);
|
|
55
|
+
return { updated: true, previousVersion: "5.0.2", currentVersion: targetVersion };
|
|
56
|
+
},
|
|
57
|
+
updateTools: async () => assert.fail("must not update tools"),
|
|
58
|
+
requestRestart: async () => { calls.push("restart"); }
|
|
59
|
+
});
|
|
60
|
+
const ctx = {
|
|
61
|
+
chat: { id: 42 },
|
|
62
|
+
callbackQuery: { data: "arisa-update:core:5.1.0", message: { message_id: 7 } },
|
|
63
|
+
answerCallbackQuery: async (payload) => { calls.push(["answer", payload]); },
|
|
64
|
+
api: {
|
|
65
|
+
editMessageText: async (...args) => { calls.push(["edit", ...args]); }
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
assert.equal(await handler(ctx), true);
|
|
70
|
+
assert.deepEqual(calls, [
|
|
71
|
+
["answer", { text: "Updating Arisa…" }],
|
|
72
|
+
["edit", 42, 7, "Updating Arisa to 5.1.0…"],
|
|
73
|
+
["updateCore", "5.1.0"],
|
|
74
|
+
["edit", 42, 7, "Arisa updated from 5.0.2 to 5.1.0. Restarting…"],
|
|
75
|
+
"restart"
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("uses official-tool-sync for the confirmed safe tool action", async () => {
|
|
80
|
+
const calls = [];
|
|
81
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
82
|
+
authorize: async () => ({ ok: true }),
|
|
83
|
+
updateCore: async () => assert.fail("must not update core"),
|
|
84
|
+
updateTools: async (chatId) => {
|
|
85
|
+
calls.push(["updateTools", chatId]);
|
|
86
|
+
return {
|
|
87
|
+
updated: ["context-vault"],
|
|
88
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
requestRestart: async () => assert.fail("tool updates do not restart Arisa")
|
|
92
|
+
});
|
|
93
|
+
const ctx = {
|
|
94
|
+
chat: { id: 42 },
|
|
95
|
+
callbackQuery: { data: "arisa-update:tools", message: { message_id: 7 } },
|
|
96
|
+
answerCallbackQuery: async (payload) => { calls.push(["answer", payload]); },
|
|
97
|
+
api: {
|
|
98
|
+
editMessageText: async (...args) => { calls.push(["edit", ...args]); }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
assert.equal(await handler(ctx), true);
|
|
103
|
+
assert.deepEqual(calls, [
|
|
104
|
+
["answer", { text: "Updating safe tools…" }],
|
|
105
|
+
["edit", 42, 7, "Updating safe official tools…"],
|
|
106
|
+
["updateTools", 42],
|
|
107
|
+
["edit", 42, 7, [
|
|
108
|
+
"Updated official tools:",
|
|
109
|
+
"- context-vault",
|
|
110
|
+
"",
|
|
111
|
+
"Not changed (needs review):",
|
|
112
|
+
"- customized [locally-modified]"
|
|
113
|
+
].join("\n")]
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("reports restart failure separately after a successful core update", async () => {
|
|
118
|
+
const edits = [];
|
|
119
|
+
const handler = createTelegramUpdateCallbackHandler({
|
|
120
|
+
authorize: async () => ({ ok: true }),
|
|
121
|
+
updateCore: async () => ({ updated: true, previousVersion: "5.0.2", currentVersion: "5.1.0" }),
|
|
122
|
+
updateTools: async () => assert.fail("must not update tools"),
|
|
123
|
+
requestRestart: async () => { throw new Error("not running as a service"); }
|
|
124
|
+
});
|
|
125
|
+
const ctx = {
|
|
126
|
+
chat: { id: 42 },
|
|
127
|
+
callbackQuery: { data: "arisa-update:core:5.1.0", message: { message_id: 7 } },
|
|
128
|
+
answerCallbackQuery: async () => {},
|
|
129
|
+
api: {
|
|
130
|
+
editMessageText: async (_chatId, _messageId, text) => { edits.push(text); }
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
assert.equal(await handler(ctx), true);
|
|
135
|
+
assert.equal(edits.at(-1), [
|
|
136
|
+
"Arisa updated to 5.1.0, but restart failed: not running as a service",
|
|
137
|
+
"Run /restart to activate it."
|
|
138
|
+
].join("\n"));
|
|
139
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { compareVersions, formatUpdateReport } from "../src/runtime/update-manager.js";
|
|
3
|
+
import { compareVersions, formatUpdateReport, installCoreUpdate, updateOfficialTools } from "../src/runtime/update-manager.js";
|
|
4
4
|
|
|
5
5
|
test("compares semantic versions", () => {
|
|
6
6
|
assert.equal(compareVersions("5.0.2", "5.0.3"), -1);
|
|
@@ -85,3 +85,59 @@ test("shortens long review status labels", () => {
|
|
|
85
85
|
assert.match(report, /campaign-draft-runner\n \[untracked\]/);
|
|
86
86
|
assert.ok(report.split("\n").every((line) => [...line].length <= 35));
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test("installs only the currently published core update and verifies it", async () => {
|
|
90
|
+
const calls = [];
|
|
91
|
+
const versions = ["5.0.2", "5.1.0"];
|
|
92
|
+
const result = await installCoreUpdate({ targetVersion: "5.1.0" }, {
|
|
93
|
+
readVersion: async () => versions.shift(),
|
|
94
|
+
fetchVersion: async () => "5.1.0",
|
|
95
|
+
execute: async (...args) => { calls.push(args); }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(calls, [["npm", ["install", "--global", "arisa@5.1.0"]]]);
|
|
99
|
+
assert.deepEqual(result, {
|
|
100
|
+
updated: true,
|
|
101
|
+
previousVersion: "5.0.2",
|
|
102
|
+
currentVersion: "5.1.0"
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("refuses a stale core update button", async () => {
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
installCoreUpdate({ targetVersion: "5.1.0" }, {
|
|
109
|
+
readVersion: async () => "5.0.2",
|
|
110
|
+
fetchVersion: async () => "5.1.1",
|
|
111
|
+
execute: async () => assert.fail("must not install a stale target")
|
|
112
|
+
}),
|
|
113
|
+
/Run \/update again/
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("updates only safe official tools and reloads the registry", async () => {
|
|
118
|
+
const calls = [];
|
|
119
|
+
const toolRegistry = {
|
|
120
|
+
async run(request) {
|
|
121
|
+
calls.push(request);
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
output: {
|
|
125
|
+
json: {
|
|
126
|
+
updates: [{ name: "context-vault", action: "updated" }],
|
|
127
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async load() { calls.push("load"); }
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(await updateOfficialTools({ chatId: 42, toolRegistry }), {
|
|
136
|
+
updated: ["context-vault"],
|
|
137
|
+
skipped: [{ name: "customized", status: "locally-modified" }]
|
|
138
|
+
});
|
|
139
|
+
assert.deepEqual(calls, [
|
|
140
|
+
{ name: "official-tool-sync", chatId: 42, request: { args: { action: "update-safe" } } },
|
|
141
|
+
"load"
|
|
142
|
+
]);
|
|
143
|
+
});
|