From 0f46d9b3bd773d8bff408f6aafbf70d76a360427 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Sat, 4 Apr 2026 03:30:39 +0000 Subject: [PATCH] test(39-02): add failing tests for voice capability detection --- .../__tests__/39-voice-hardware-probe.test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 server/src/__tests__/39-voice-hardware-probe.test.ts diff --git a/server/src/__tests__/39-voice-hardware-probe.test.ts b/server/src/__tests__/39-voice-hardware-probe.test.ts new file mode 100644 index 00000000..05bd00cf --- /dev/null +++ b/server/src/__tests__/39-voice-hardware-probe.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock systeminformation before any imports that use it +vi.mock("systeminformation", () => ({ + default: { + graphics: vi.fn().mockResolvedValue({ controllers: [] }), + }, +})); + +// Mock child_process execFile before importing hardware service +vi.mock("node:child_process", () => { + const execFileCb = vi.fn(); + return { execFile: execFileCb }; +}); + +// Mock node:os so we can control RAM/CPU model +vi.mock("node:os", () => { + const osActual = { + totalmem: vi.fn().mockReturnValue(16 * 1024 * 1024 * 1024), + freemem: vi.fn().mockReturnValue(8 * 1024 * 1024 * 1024), + cpus: vi.fn().mockReturnValue([{ model: "Intel Core i9" }]), + }; + return { default: osActual, ...osActual }; +}); + +import { execFile } from "node:child_process"; +import os from "node:os"; +import si from "systeminformation"; +import { hardwareService, _resetHardwareCache } from "../services/hardware.js"; + +// Helper: make execFile resolve for specific commands +function mockExecFilePartial(resolveFor: string[]) { + (execFile as ReturnType).mockImplementation( + (c: string, _args: string[], _opts: unknown, cb: (err: null | Error, stdout: string, stderr: string) => void) => { + if (resolveFor.includes(c)) { + cb(null, "version output", ""); + } else { + const err = Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + cb(err as Error, "", ""); + } + } + ); +} + +// Helper: make execFile reject with ENOENT for all +function mockExecFileAllFail() { + (execFile as ReturnType).mockImplementation( + (_c: string, _args: string[], _opts: unknown, cb: (err: Error, stdout: string, stderr: string) => void) => { + const err = Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + cb(err, "", ""); + } + ); +} + +describe("voice capability detection", () => { + beforeEach(() => { + vi.resetAllMocks(); + // Re-stub si.graphics after resetAllMocks clears the mock + vi.mocked(si.graphics).mockResolvedValue({ controllers: [] } as Awaited>); + // Restore default os mocks + vi.mocked(os.totalmem).mockReturnValue(16 * 1024 * 1024 * 1024); + vi.mocked(os.freemem).mockReturnValue(8 * 1024 * 1024 * 1024); + vi.mocked(os.cpus).mockReturnValue([{ model: "Intel Core i9" } as ReturnType[0]]); + _resetHardwareCache(); + }); + + it("returns whisperAvailable=true, piperAvailable=true when both binaries resolve", async () => { + mockExecFilePartial(["whisper-cpp", "piper"]); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.voiceCapability.whisperAvailable).toBe(true); + expect(info.voiceCapability.piperAvailable).toBe(true); + }); + + it("returns whisperAvailable=false, piperAvailable=false when both binaries throw ENOENT", async () => { + mockExecFileAllFail(); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.voiceCapability.whisperAvailable).toBe(false); + expect(info.voiceCapability.piperAvailable).toBe(false); + }); + + it("returns whisperAvailable=true, piperAvailable=false when only whisper-cpp is found", async () => { + mockExecFilePartial(["whisper-cpp"]); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.voiceCapability.whisperAvailable).toBe(true); + expect(info.voiceCapability.piperAvailable).toBe(false); + }); + + it("returns whisperAvailable=true when whisper (fallback) resolves but whisper-cpp fails", async () => { + mockExecFilePartial(["whisper"]); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.voiceCapability.whisperAvailable).toBe(true); + }); + + it("sets voiceTierSufficient=false for cpu_only tier with < 4GB free RAM", async () => { + mockExecFileAllFail(); + + vi.mocked(os.freemem).mockReturnValue(2 * 1024 * 1024 * 1024); // 2GB free + vi.mocked(os.totalmem).mockReturnValue(8 * 1024 * 1024 * 1024); + vi.mocked(os.cpus).mockReturnValue([{ model: "Intel Core i5" } as ReturnType[0]]); + + // linux = non-Apple, cpu_only path + const savedPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "linux", configurable: true }); + + _resetHardwareCache(); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.hardwareTier).toBe("cpu_only"); + expect(info.voiceCapability.voiceTierSufficient).toBe(false); + + Object.defineProperty(process, "platform", { value: savedPlatform, configurable: true }); + }); + + it("sets voiceTierSufficient=true for apple_silicon tier", async () => { + mockExecFileAllFail(); + + vi.mocked(os.totalmem).mockReturnValue(16 * 1024 * 1024 * 1024); + vi.mocked(os.freemem).mockReturnValue(8 * 1024 * 1024 * 1024); + vi.mocked(os.cpus).mockReturnValue([{ model: "Apple M4" } as ReturnType[0]]); + + const savedPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "darwin", configurable: true }); + + _resetHardwareCache(); + const svc = hardwareService(); + const info = await svc.detect(); + expect(info.hardwareTier).toBe("apple_silicon"); + expect(info.voiceCapability.voiceTierSufficient).toBe(true); + + Object.defineProperty(process, "platform", { value: savedPlatform, configurable: true }); + }); +});