Everything the plugin is, moved out of officerdev/platform on 2026-08-15 — 41 files, unchanged from the tree they left. manifest.ts identity, one permission, ffmpeg/ffprobe declared api/ the sidecar proxy; the prefix comes from mountPrefix() sidecar/ the whole /api/music contract — indexing, streaming, per-user state db/ music_favorites, _playlists, _playlist_items, _now_playing web/ panels, layout, and the player: engine, bar, lyrics, favourites cliamp/ the second playback path, parked — not working, kept deliberately widgets/ the dashboard widget, parked — plugins cannot contribute widgets assets/ icon.png, the dock tile scripts/ the reindex CLI PLUGIN.md is the design record: what moved, what stayed, what broke, and why. MUSIC_API.md is the contract the phone and tablet apps speak, and the reason the sidecar's HTTP shape is not free to change. ── It does not build here, and that is the point ── The platform resolves `hooks/useClient`, `officerdev`, `officerdb/db` and `@@/*` through the workspace links in its own node_modules. Measured from this directory, outside the platform checkout, every one of them fails to resolve — 7 imports in the backend, ~29 in the frontend. So this repository is the source of truth, not yet a buildable unit. Making it one means the host API becoming something a plugin can depend on rather than something it reaches into. That is the next problem, and having the code here is what makes it unavoidable rather than theoretical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { cliampUpgradeData, musicWebsocket } from './cliamp-ws';
|
|
|
|
// The player socket refuses a path before it spawns anything, so these two cases exercise the whole
|
|
// server → handler → frame path without starting cliamp. Anything that would actually play needs a real
|
|
// file and a real audio sink, so it is not tested here.
|
|
|
|
function serveOnce() {
|
|
const server = Bun.serve({
|
|
port: 0,
|
|
hostname: '127.0.0.1',
|
|
fetch(req, srv) {
|
|
const url = new URL(req.url);
|
|
const data = cliampUpgradeData(url.pathname, url.searchParams);
|
|
if (data && srv.upgrade(req, { data })) return undefined as unknown as Response;
|
|
return new Response('nope', { status: 400 });
|
|
},
|
|
websocket: musicWebsocket,
|
|
});
|
|
return server;
|
|
}
|
|
|
|
function firstFrame(url: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(url);
|
|
const timer = setTimeout(() => reject(new Error('no frame')), 3000);
|
|
ws.addEventListener('message', (ev) => {
|
|
clearTimeout(timer);
|
|
ws.close();
|
|
resolve(String(ev.data));
|
|
});
|
|
ws.addEventListener('error', () => {
|
|
clearTimeout(timer);
|
|
reject(new Error('socket error'));
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('cliamp player socket', () => {
|
|
it('rejects a path that escapes the owner home', async () => {
|
|
const server = serveOnce();
|
|
try {
|
|
const frame = await firstFrame(`ws://127.0.0.1:${server.port}/cliamp/ws?files=../../etc/passwd`);
|
|
expect(JSON.parse(frame)).toEqual({ type: 'output', data: '\r\n[Error] Invalid file path.\r\n' });
|
|
} finally {
|
|
server.stop(true);
|
|
}
|
|
});
|
|
|
|
it('reports a missing files param instead of spawning', async () => {
|
|
const server = serveOnce();
|
|
try {
|
|
const frame = await firstFrame(`ws://127.0.0.1:${server.port}/cliamp/ws`);
|
|
expect(JSON.parse(frame)).toEqual({ type: 'output', data: '\r\n[Error] No files specified.\r\n' });
|
|
} finally {
|
|
server.stop(true);
|
|
}
|
|
});
|
|
|
|
it('routes only the two cliamp paths', () => {
|
|
const q = new URLSearchParams();
|
|
expect(cliampUpgradeData('/cliamp/ws', q)).toEqual({ kind: 'player', files: '' });
|
|
expect(cliampUpgradeData('/cliamp/audio/ws', q)).toEqual({ kind: 'capture' });
|
|
expect(cliampUpgradeData('/stream', q)).toBeNull();
|
|
});
|
|
});
|