test(39-02): add failing tests for voice capability detection
This commit is contained in:
parent
de544a6dde
commit
0f46d9b3bd
1 changed files with 136 additions and 0 deletions
136
server/src/__tests__/39-voice-hardware-probe.test.ts
Normal file
136
server/src/__tests__/39-voice-hardware-probe.test.ts
Normal file
|
|
@ -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<typeof vi.fn>).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<typeof vi.fn>).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<ReturnType<typeof si.graphics>>);
|
||||
// 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<typeof os.cpus>[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<typeof os.cpus>[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<typeof os.cpus>[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 });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue