import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { existsSync, readFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repo = path.resolve(fileURLToPath(new URL('..', import.meta.url))); function run(command, args, env) { return new Promise((resolve, reject) => { const child = spawn(command, args, { env, cwd: repo }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk; }); child.stderr.on('data', (chunk) => { stderr += chunk; }); child.on('error', reject); child.on('close', (code) => { if (code === 0) resolve({ stdout, stderr }); else reject(new Error(`install failed (${code}): ${stderr || stdout}`)); }); }); } test('install.sh replaces leftover files from an older version and keeps config', async () => { const work = await mkdtemp(path.join(os.tmpdir(), 'jarvis-install-')); const home = path.join(work, 'home'); const root = path.join(work, 'src'); const bin = path.join(work, 'bin'); const log = path.join(work, 'commands.log'); const machine = (await run('uname', ['-m'], process.env)).stdout.trim(); const bareName = /aarch64|arm64/.test(machine) ? 'bare-runtime-linux-arm64' : 'bare-runtime-linux-x64'; try { await mkdir(path.join(root, 'packaging'), { recursive: true }); await mkdir(path.join(root, 'apps/gnome-extension'), { recursive: true }); await mkdir(path.join(root, 'node_modules', bareName, 'bin'), { recursive: true }); await mkdir(bin, { recursive: true }); await cp(path.join(repo, 'packaging/install.sh'), path.join(root, 'packaging/install.sh')); await cp(path.join(repo, 'packaging/jarvisd.service'), path.join(root, 'packaging/jarvisd.service')); await cp(path.join(repo, 'packaging/qvac.config.template.json'), path.join(root, 'packaging/qvac.config.template.json')); await cp(path.join(repo, 'apps/gnome-extension/jarvis@qvac.local'), path.join(root, 'apps/gnome-extension/jarvis@qvac.local'), { recursive: true }); await writeFile(path.join(root, 'node_modules', bareName, 'bin', 'bare'), '#!/bin/sh\nexit 0\n'); await chmod(path.join(root, 'node_modules', bareName, 'bin', 'bare'), 0o755); await writeFile(path.join(root, 'daemon-marker'), 'new-tree\n'); const appDir = path.join(home, '.local/share/jarvis-qvac'); const extDir = path.join(home, '.local/share/gnome-shell/extensions/jarvis@qvac.local'); const configDir = path.join(home, '.config/jarvis'); await mkdir(path.join(appDir, 'node_modules/old-pkg'), { recursive: true }); await mkdir(extDir, { recursive: true }); await mkdir(configDir, { recursive: true }); await mkdir(path.join(home, '.local/share/jarvis-qvac.staging.oldfail'), { recursive: true }); await writeFile(path.join(appDir, 'stale-old-version.js'), 'leftover from previous install\n'); await writeFile(path.join(extDir, 'old-extension.js'), 'leftover extension\n'); await writeFile(path.join(configDir, 'config.json'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n'); await writeFile(path.join(home, '.local/share/jarvis-qvac.staging.oldfail/junk'), 'failed staging\n'); await writeFile(path.join(bin, 'systemctl'), `#!/bin/sh echo "systemctl $*" >> '${log}' if [ "$2" = is-active ]; then exit 0; fi exit 0 `); await writeFile(path.join(bin, 'gnome-extensions'), `#!/bin/sh echo "gnome-extensions $*" >> '${log}' if [ "$1" = install ]; then exit 1; fi if [ "$1" = info ]; then printf '%s\\n' 'Enabled: Yes' 'State: ACTIVE'; exit 0; fi exit 0 `); await writeFile(path.join(bin, 'gsettings'), `#!/bin/sh echo "gsettings $*" >> '${log}' if [ "$1" = get ]; then printf "%s\\n" "['jarvis@qvac.local']"; fi exit 0 `); await writeFile(path.join(bin, 'gdbus'), `#!/bin/sh echo "gdbus $*" >> '${log}' exit 0 `); await writeFile(path.join(bin, 'logger'), `#!/bin/sh echo "logger $*" >> '${log}' exit 0 `); await chmod(path.join(bin, 'systemctl'), 0o755); await chmod(path.join(bin, 'gnome-extensions'), 0o755); await chmod(path.join(bin, 'gsettings'), 0o755); await chmod(path.join(bin, 'gdbus'), 0o755); await chmod(path.join(bin, 'logger'), 0o755); const result = await run('bash', [path.join(root, 'packaging/install.sh'), '--enable'], { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}`, XDG_DATA_HOME: path.join(home, '.local/share'), XDG_CACHE_HOME: path.join(home, '.cache'), XDG_STATE_HOME: path.join(home, '.local/state'), }); assert.equal(existsSync(path.join(appDir, 'stale-old-version.js')), false); assert.equal(existsSync(path.join(appDir, 'node_modules/old-pkg')), false); assert.equal(existsSync(path.join(home, '.local/share/jarvis-qvac.staging.oldfail')), false); assert.equal(existsSync(path.join(extDir, 'old-extension.js')), false); assert.equal(existsSync(path.join(extDir, 'extension.js')), true); assert.equal(await readFile(path.join(appDir, 'daemon-marker'), 'utf8'), 'new-tree\n'); assert.equal(await readFile(path.join(configDir, 'config.json'), 'utf8'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n'); const unit = await readFile(path.join(home, '.config/systemd/user/jarvisd.service'), 'utf8'); assert.match(unit, /bare-runtime-linux/); assert.doesNotMatch(unit, /\/usr\/bin\/node/); const commands = await readFile(log, 'utf8'); assert.match(commands, /systemctl --user stop jarvisd\.service/); assert.match(commands, /gnome-extensions enable jarvis@qvac\.local/); assert.match(commands, /gdbus call .*ReloadExtension/); assert.match(commands, /gdbus call .*EnableExtension/); assert.match(commands, /systemctl --user restart jarvisd\.service/); const installLog = await readFile(path.join(home, '.local/state/jarvis/install.log'), 'utf8'); assert.match(installLog, /stopping running jarvisd\.service/); assert.match(installLog, /removing leftover staging directory/); assert.match(installLog, /removing previous application tree/); assert.match(installLog, /copied GNOME extension files/); assert.match(installLog, /reloaded jarvis@qvac\.local in the running GNOME Shell/); assert.match(installLog, /enabled GNOME extension jarvis@qvac\.local/); assert.match(installLog, /kept existing .*config\.json/); assert.match(installLog, /install complete; log file/); assert.doesNotMatch(installLog, /will enable Jarvis after the next login/); assert.match(result.stdout, /jarvis-install: stopping running jarvisd\.service/); } finally { await rm(work, { recursive: true, force: true }); } }); test('first-run keeps TTS enabled even when the preview is skipped', () => { const source = readFileSync(new URL('../packaging/first-run.sh', import.meta.url), 'utf8'); assert.match(source, /Play a TTS preview now\?/); assert.match(source, /TTS_ENABLED=true/); assert.doesNotMatch(source, /Enable TTS preview\?/); assert.doesNotMatch(source, /TTS_ENABLED=false/); assert.match(source, /spoken replies remain enabled/); });