fix(cli): publish cached synthesis script exclusively (#3722)

This commit is contained in:
James Russo
2026-09-05 20:05:15 -04:00
committed by GitHub
parent b49fba8dea
commit cfdaccb7e6
2 changed files with 109 additions and 2 deletions
@@ -0,0 +1,106 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
const paths = vi.hoisted(() => ({ home: "" }));
vi.mock("node:os", async (importOriginal) => ({
...(await importOriginal<typeof import("node:os")>()),
homedir: () => paths.home,
}));
vi.mock("node:fs", async (importOriginal) => {
const original = await importOriginal<typeof fs>();
return { ...original, existsSync: vi.fn(original.existsSync) };
});
vi.mock("node:child_process", () => ({ execFileSync: vi.fn() }));
vi.mock("./python.js", () => ({
findPython: () => "test-python",
hasPythonPackage: () => true,
}));
vi.mock("./manager.js", async (importOriginal) => ({
...(await importOriginal<typeof import("./manager.js")>()),
ensureModel: vi.fn().mockResolvedValue("model.onnx"),
ensureVoices: vi.fn().mockResolvedValue("voices.bin"),
}));
describe("synthesis script cache", () => {
let cacheDir: string;
let scriptPath: string;
let outputPath: string;
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
paths.home = fs.mkdtempSync(join(tmpdir(), "hf-tts-cache-"));
cacheDir = join(paths.home, ".cache", "hyperframes", "tts");
scriptPath = join(cacheDir, "synth-v3.py");
outputPath = join(paths.home, "speech.wav");
fs.mkdirSync(cacheDir, { recursive: true });
vi.mocked(execFileSync).mockImplementation(() => {
fs.writeFileSync(outputPath, "stub audio");
return JSON.stringify({
outputPath,
sampleRate: 24000,
durationSeconds: 1,
langApplied: true,
});
});
});
afterEach(() => {
expect(fs.readdirSync(cacheDir).filter((name) => name.startsWith(".hf-create-"))).toEqual([]);
fs.rmSync(paths.home, { recursive: true, force: true });
});
it("creates the script, removes only old script versions, and reuses the current cache", async () => {
fs.writeFileSync(join(cacheDir, "synth-v2.py"), "old script");
fs.writeFileSync(join(cacheDir, "notes.txt"), "keep");
const { synthesize } = await import("./synthesize.js");
await synthesize("Hello", outputPath);
const script = fs.readFileSync(scriptPath, "utf-8");
expect(script).toContain("import kokoro_onnx");
expect(script).toContain('"durationSeconds": round(duration, 3)');
expect(fs.existsSync(join(cacheDir, "synth-v2.py"))).toBe(false);
expect(fs.readFileSync(join(cacheDir, "notes.txt"), "utf-8")).toBe("keep");
fs.writeFileSync(scriptPath, "existing current script");
await synthesize("Again", outputPath);
expect(fs.readFileSync(scriptPath, "utf-8")).toBe("existing current script");
});
it.each(["concurrent file", "dangling symlink"])(
"preserves a %s at publication",
async (kind) => {
const target = join(paths.home, "missing.py");
const original = await vi.importActual<typeof fs>("node:fs");
let injected = false;
vi.mocked(fs.existsSync).mockImplementation((path) => {
if (path === scriptPath && !injected) {
injected = true;
if (kind === "dangling symlink") fs.symlinkSync(target, scriptPath);
else fs.writeFileSync(scriptPath, "concurrent script");
return false;
}
return original.existsSync(path);
});
try {
const { synthesize } = await import("./synthesize.js");
await synthesize("Hello", outputPath);
expect(injected).toBe(true);
if (kind === "dangling symlink") {
expect(fs.lstatSync(scriptPath).isSymbolicLink()).toBe(true);
expect(fs.existsSync(target)).toBe(false);
} else {
expect(fs.readFileSync(scriptPath, "utf-8")).toBe("concurrent script");
}
expect(execFileSync).toHaveBeenCalledWith(
"test-python",
expect.arrayContaining([scriptPath, "model.onnx", "voices.bin", "Hello", outputPath]),
expect.objectContaining({ timeout: 300_000 }),
);
} finally {
vi.mocked(fs.existsSync).mockImplementation(original.existsSync);
}
},
);
});
+3 -2
View File
@@ -1,6 +1,6 @@
// fallow-ignore-file complexity
import { execFileSync } from "node:child_process";
import { existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
import { join, dirname, basename } from "node:path";
import { homedir } from "node:os";
import {
@@ -11,6 +11,7 @@ import {
type SupportedLang,
} from "./manager.js";
import { findPython, hasPythonPackage } from "./python.js";
import { writeNewFileSync } from "../utils/writeNewFile.js";
// ---------------------------------------------------------------------------
// Inline Python script for Kokoro synthesis
@@ -100,7 +101,7 @@ const SCRIPT_PATH = join(SCRIPT_DIR, "synth-v3.py");
function ensureSynthScript(): string {
if (!existsSync(SCRIPT_PATH)) {
mkdirSync(SCRIPT_DIR, { recursive: true });
writeFileSync(SCRIPT_PATH, SYNTH_SCRIPT);
writeNewFileSync(SCRIPT_PATH, SYNTH_SCRIPT);
// Best-effort: delete older versioned scripts left behind by previous
// CLI releases so users don't accumulate stale files in ~/.cache.
const currentName = basename(SCRIPT_PATH);