// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // Run: node desktop/modules/assistant/test_config.js // Config.js is a QML library, so its .pragma line is stripped before node // evaluates it. const fs = require("fs"); const assert = require("assert"); const src = fs.readFileSync(__dirname + "/Config.js", "utf8") .replace(/^\.pragma library$/m, ""); const C = new Function(src + "\nreturn { scalars, setScalar, env, setEnv, quote, clouds, setClouds, cloudError };")(); const KEYS = ["llm", "system", "tool_rounds", "temperature", "thinking"]; const file = [ "# desktop-assistant configuration.", "# Style name = weight; prose with an equals sign", "", '# llm = "http://localhost:8181/v1" # llama-server, OpenAI API', '# system = "You are a voice assistant. ..."', "temperature = 0.6 # per request; unset = the preset's", "", "# [whisper]", "# port = 8182", "", "[env]", 'SEARXNG_URL = "http://search.example.org" # a # inside a comment', 'MD_EDITOR = "typora"', '# WEATHER_ENV = "/home/you/x"', "", ].join("\n"); // Only active keys have values; prose and commented keys do not. assert.deepStrictEqual(C.scalars(file, KEYS), { temperature: 0.6 }); assert.deepStrictEqual(C.env(file), [ { key: "SEARXNG_URL", value: "http://search.example.org" }, { key: "MD_EDITOR", value: "typora" }, ]); // Setting an active key keeps its inline comment. let t = C.setScalar(file, "temperature", "0.2"); assert.strictEqual(t.split("\n")[5], "temperature = 0.2 # per request; unset = the preset's"); // Unsetting comments it out in place, and it reads as unset. t = C.setScalar(file, "temperature", null); assert.strictEqual(t.split("\n")[5], "# temperature = 0.6 # per request; unset = the preset's"); assert.deepStrictEqual(C.scalars(t, KEYS), {}); // Setting a commented key activates its line rather than adding one. t = C.setScalar(file, "llm", C.quote("http://box:8080/v1")); assert.strictEqual(t.split("\n")[3], 'llm = "http://box:8080/v1" # llama-server, OpenAI API'); assert.strictEqual(t.split("\n").length, file.split("\n").length); // A key nowhere in the file lands before the first table, not inside it. t = C.setScalar(file, "thinking", "false"); const lines = t.split("\n"); assert.ok(lines.indexOf("thinking = false") < lines.indexOf("[env]")); assert.strictEqual(C.scalars(t, KEYS).thinking, false); // The prose line with "=" is never treated as a key. assert.strictEqual(C.setScalar(file, "name", null), file); // Strings round-trip with quotes, backslashes and newlines. const tricky = 'Say "hi"\\now\nline two\ttab'; t = C.setScalar(file, "system", C.quote(tricky)); assert.strictEqual(C.scalars(t, KEYS).system, tricky); assert.strictEqual(t.split("\n").length, file.split("\n").length); // env: replace, remove, add after the last active entry. t = C.setEnv(file, "MD_EDITOR", "xdg-open"); assert.deepStrictEqual(C.env(t)[1], { key: "MD_EDITOR", value: "xdg-open" }); t = C.setEnv(file, "SEARXNG_URL", null); assert.deepStrictEqual(C.env(t).map(e => e.key), ["MD_EDITOR"]); t = C.setEnv(file, "CALENDAR_DIR", "/home/you/cal"); assert.deepStrictEqual(C.env(t).map(e => e.key), ["SEARXNG_URL", "MD_EDITOR", "CALENDAR_DIR"]); assert.strictEqual(t.split("\n")[13], 'CALENDAR_DIR = "/home/you/cal"'); // No [env] table yet: one is appended. t = C.setEnv("temperature = 0.6\n", "X", "1"); assert.deepStrictEqual(C.env(t), [{ key: "X", value: "1" }]); assert.ok(/\n\[env\]\nX = "1"\n$/.test(t), JSON.stringify(t)); // An env key in another table is not the env one. assert.deepStrictEqual(C.env('[voice]\nlang = "it"\n'), []); // [[cloud]] tables. const withCloud = file + [ "# Cloud models.", "[[cloud]]", 'name = "hetzner"', 'url = "https://llm.example.org/v1" # a comment', 'key_cmd = "pass llm/x"', 'models = ["a/b", "c:d"]', "", "[[cloud]]", 'name = "two"', 'url = "u"', 'key_cmd = "k"', "models = []", "", ].join("\n"); assert.deepStrictEqual(C.clouds(withCloud), [ { name: "hetzner", url: "https://llm.example.org/v1", key_cmd: "pass llm/x", models: ["a/b", "c:d"] }, { name: "two", url: "u", key_cmd: "k", models: [] }]); assert.deepStrictEqual(C.clouds(file), []); assert.deepStrictEqual(C.env(withCloud), C.env(file)); // [env] ends at the table // A table this cannot read in full is not offered for editing. assert.strictEqual(C.clouds(withCloud.replace('models = ["a/b", "c:d"]', 'models = [\n "a/b",\n]')), null); assert.strictEqual(C.clouds(withCloud.replace('name = "two"', 'name = "two"\nextra = 1')), null); // Rewritten at the end of the file; everything else stays. const one = C.setClouds(withCloud, [{ name: "n", url: "u", key_cmd: 'pass "q"', models: ["m"] }]); assert.deepStrictEqual(C.clouds(one), [{ name: "n", url: "u", key_cmd: 'pass "q"', models: ["m"] }]); assert.deepStrictEqual(C.env(one), C.env(file)); assert.deepStrictEqual(C.scalars(one, KEYS), C.scalars(file, KEYS)); assert.ok(one.includes("# Cloud models.") && one.endsWith('models = ["m"]\n'), one); assert.deepStrictEqual(C.clouds(C.setClouds(one, [])), []); // What the daemon refuses to start with. const ok = { name: "a", url: "u", key_cmd: "k", models: [] }; assert.strictEqual(C.cloudError([ok]), ""); assert.strictEqual(C.cloudError([]), ""); for (const bad of [{ name: "" }, { name: "a:b" }, { url: "" }, { key_cmd: " " }]) assert.notStrictEqual(C.cloudError([Object.assign({}, ok, bad)]), "", JSON.stringify(bad)); assert.notStrictEqual(C.cloudError([ok, ok]), ""); console.log("test_config: ok");