W / test /langserver-binary-update.test.js
Ac66's picture
Upload folder using huggingface_hub
2b64d42 verified
// Dashboard /langserver/binary + /langserver/update endpoints.
//
// Issues #7, #10, #49, #87 collectively asked for "how do I get / update
// the language_server_linux_x64 binary?" There's a working install-ls.sh
// shipped in the docker image but every user had to docker-exec into the
// container, run it, then bounce the LS pool by hand. The dashboard now
// exposes:
//
// GET /dashboard/api/langserver/binary → current size/mtime/sha256
// POST /dashboard/api/langserver/update → run install-ls.sh + restart
//
// Static-validate both routes; the live install runs against GitHub
// releases and Exafunction so it's not safe to hit in unit tests.
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const API_JS = readFileSync(join(__dirname, '..', 'src/dashboard/api.js'), 'utf8');
const LS_JS = readFileSync(join(__dirname, '..', 'src/langserver.js'), 'utf8');
describe('LS binary inspect endpoint (#7/#10/#49)', () => {
test('GET /langserver/binary returns size + mtime + sha256 prefix', () => {
const m = API_JS.match(/subpath === '\/langserver\/binary'[\s\S]+?\n \}/);
assert.ok(m, 'GET /langserver/binary route not found');
const route = m[0];
assert.match(route, /method === 'GET'/);
assert.match(route, /sizeBytes:/);
assert.match(route, /mtime:/);
assert.match(route, /sha256:/);
assert.match(route, /createHash\('sha256'\)/,
'must hash via node:crypto, not shell out to sha256sum');
assert.match(route, /\.slice\(0, 16\)/,
'sha256 should be truncated to 16 hex chars (matches install-ls.sh log)');
});
});
describe('LS binary update endpoint (#7/#10/#49/#87)', () => {
test('POST /langserver/update spawns install-ls.sh', () => {
const m = API_JS.match(/subpath === '\/langserver\/update'[\s\S]+?\n \}/);
assert.ok(m, 'POST /langserver/update route not found');
const route = m[0];
assert.match(route, /method === 'POST'/);
assert.match(route, /spawn\b/);
assert.match(route, /install-ls\.sh/);
assert.match(route, /LS_INSTALL_PATH:\s*config\.lsBinaryPath/,
'must point install-ls.sh at the configured binary path so the right file is overwritten');
});
test('rejects custom URLs from non-allowlisted hosts', () => {
const m = API_JS.match(/subpath === '\/langserver\/update'[\s\S]+?\n \}/);
assert.ok(m);
const route = m[0];
// Defence-in-depth: dashboard auth gates the endpoint, but we also
// refuse to feed an arbitrary URL into the install script. Without
// this guard, an attacker who got past dashboard auth could write
// arbitrary bytes to the LS binary and have node exec them.
assert.match(route, /protocol !== 'https:'/,
'must require https');
assert.match(route, /allowedHosts/,
'must consult an allowlist of hosts, not arbitrary URLs');
assert.match(route, /github\.com/,
'github.com must be in the allowlist (our releases live there)');
});
test('after install, every LS pool entry is restarted', () => {
const m = API_JS.match(/subpath === '\/langserver\/update'[\s\S]+?\n \}/);
assert.ok(m);
const route = m[0];
assert.match(route, /_poolKeys/,
'must enumerate all live LS pool keys (default + per-proxy entries)');
assert.match(route, /restartLsForProxy/,
'must call restartLsForProxy on each entry to swap the binary in flight');
});
test('langserver.js exports _poolKeys + getProxyByKey for the update endpoint', () => {
assert.match(LS_JS, /export function _poolKeys\(\)/);
assert.match(LS_JS, /export function getProxyByKey\(key\)/);
});
});
// User report (2026-05-01): "LS update has no effect" — the toast
// always said "restarted N instances" without telling the user whether
// the binary itself actually changed. Pool was often cold (proxy hadn't
// served a request yet) so N=0, and same-version re-download was
// indistinguishable from a real update.
describe('LS update result is descriptive enough to debug "no effect" reports', () => {
test('captures sha256 BEFORE and AFTER the install-ls.sh run', () => {
const m = API_JS.match(/subpath === '\/langserver\/update'[\s\S]+?\n \}/);
assert.ok(m);
const route = m[0];
// Both snapshots must come from the same hashing path (sha256 of
// the binary file at config.lsBinaryPath). Pin both exist.
assert.match(route, /beforeSha\s*=\s*null/,
'must declare beforeSha BEFORE running the install script');
assert.match(route, /afterSha\s*=\s*null/,
'must declare afterSha AFTER running the install script');
assert.match(route, /binaryChanged\s*=\s*!!\(beforeSha && afterSha && beforeSha !== afterSha\)/,
'must compute binaryChanged so the dashboard can distinguish "no change" from "updated"');
});
test('response includes beforeSha / afterSha / binaryChanged / poolEmpty', () => {
const m = API_JS.match(/subpath === '\/langserver\/update'[\s\S]+?\n \}/);
assert.ok(m);
const route = m[0];
assert.match(route, /beforeSha:\s*beforeSha/,
'response payload must surface beforeSha');
assert.match(route, /afterSha:\s*afterSha/,
'response payload must surface afterSha');
assert.match(route, /binaryChanged,/,
'response payload must surface the binaryChanged flag');
assert.match(route, /poolEmpty:\s*restarted === 0 && restartErrors\.length === 0/,
'poolEmpty distinguishes "cold pool" from "all restarts failed"');
});
test('dashboard toast picks the right message for each outcome', () => {
const HTML = readFileSync(join(__dirname, '..', 'src/dashboard/index.html'), 'utf8');
const m = HTML.match(/async updateLsBinary\(\)[\s\S]+?\n \},/);
assert.ok(m, 'updateLsBinary not found in dashboard');
const fn = m[0];
// Three distinct toast keys cover the three meaningfully-different
// outcomes. A future refactor that drops any of these would silently
// regress the "no effect" UX bug.
assert.match(fn, /lsBinaryAlreadyCurrent/,
'must show a distinct toast when the binary did not change (no upstream release)');
assert.match(fn, /lsBinaryUpdatedColdPool/,
'must show a distinct toast when the binary changed but no live LS was running');
assert.match(fn, /lsBinaryUpdated/,
'must keep the regular toast for binary-changed + live-restart-succeeded');
assert.match(fn, /r\.binaryChanged/,
'toast selection must key on the binaryChanged flag');
assert.match(fn, /r\.poolEmpty/,
'toast selection must key on the poolEmpty flag');
});
});