feat(spotify): add go-librespot binary resolver + Linux support gate

Mirror youtube.ts findYtDlp/checkYtDlpAvailable (bin/ then PATH,
cache-positive-only availability, reset test hook) and add
isGoLibrespotSupported() Linux gate for the Stage 2 audio backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-07-02 20:33:42 +08:00
1 parent 978e6ee7f2
commit 32718f0118
2 files changed
+202

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { join } from "node:path";
import {
isGoLibrespotSupported,
pickGoLibrespotPath,
findGoLibrespot,
checkGoLibrespotAvailable,
resetGoLibrespotBinaryCache,
__setGoLibrespotVersionProbe,
} from "./binary.js";
const origPlatform = process.platform;
function setPlatform(p: NodeJS.Platform): void {
Object.defineProperty(process, "platform", { value: p, configurable: true });
}
afterEach(() => {
setPlatform(origPlatform);
__setGoLibrespotVersionProbe(null);
resetGoLibrespotBinaryCache();
});
describe("isGoLibrespotSupported", () => {
it("is true only on linux", () => {
setPlatform("linux");
expect(isGoLibrespotSupported()).toBe(true);
setPlatform("win32");
expect(isGoLibrespotSupported()).toBe(false);
setPlatform("darwin");
expect(isGoLibrespotSupported()).toBe(false);
});
});
describe("pickGoLibrespotPath (bin/ then PATH ordering)", () => {
const binPath = join("some", "root", "bin", "go-librespot");
it("prefers the bin/ path when the file exists", () => {
expect(
pickGoLibrespotPath([binPath, "go-librespot"], (p) => p === binPath),
).toBe(binPath);
});
it("falls through to the bare PATH name when the bin/ file is missing", () => {
expect(pickGoLibrespotPath([binPath, "go-librespot"], () => false)).toBe(
"go-librespot",
);
});
it("returns bare command names without touching the filesystem", () => {
const exists = vi.fn(() => false);
expect(pickGoLibrespotPath(["go-librespot"], exists)).toBe("go-librespot");
expect(exists).not.toHaveBeenCalled();
});
});
describe("findGoLibrespot", () => {
it("returns the bare command name when bin/go-librespot is absent", () => {
// No go-librespot binary is committed under bin/, so resolution must
// fall back to the bare PATH name (execFile resolves it at run time).
expect(findGoLibrespot()).toBe("go-librespot");
});
});
describe("checkGoLibrespotAvailable", () => {
it("returns false immediately on unsupported platforms without probing", async () => {
setPlatform("darwin");
const probe = vi.fn(async () => {});
__setGoLibrespotVersionProbe(probe);
expect(await checkGoLibrespotAvailable()).toBe(false);
expect(probe).not.toHaveBeenCalled();
});
it("returns true when the binary responds to --version on linux", async () => {
setPlatform("linux");
__setGoLibrespotVersionProbe(async () => {});
expect(await checkGoLibrespotAvailable()).toBe(true);
});
it("caches only positive results and probes once", async () => {
setPlatform("linux");
const probe = vi.fn(async () => {});
__setGoLibrespotVersionProbe(probe);
expect(await checkGoLibrespotAvailable()).toBe(true);
expect(await checkGoLibrespotAvailable()).toBe(true);
expect(probe).toHaveBeenCalledTimes(1);
});
it("does not cache a failed probe (retries on the next call)", async () => {
setPlatform("linux");
__setGoLibrespotVersionProbe(async () => {
throw new Error("ENOENT");
});
expect(await checkGoLibrespotAvailable()).toBe(false);
// A later successful probe must now succeed — negatives are not cached.
__setGoLibrespotVersionProbe(async () => {});
expect(await checkGoLibrespotAvailable()).toBe(true);
});
it("resetGoLibrespotBinaryCache clears a cached positive", async () => {
setPlatform("linux");
__setGoLibrespotVersionProbe(async () => {});
expect(await checkGoLibrespotAvailable()).toBe(true);
resetGoLibrespotBinaryCache();
__setGoLibrespotVersionProbe(async () => {
throw new Error("gone");
});
expect(await checkGoLibrespotAvailable()).toBe(false);
});
});