fix(cron): isolate fresh cron session state

* fix(cron): isolate fresh cron session state

* fix(cron): deep-copy isolated session state

* fix(cron): reset isolated session context

* test(providers): avoid shared mock races

* test(providers): type injected stream fakes

* ci: refresh package boundary on reply runtime changes

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vincent Koc
2026-04-24 22:15:19 -07:00
committed by GitHub
parent 7a9584f0f9
commit f0ceb4b68f
13 changed files with 595 additions and 154 deletions

View File

@@ -1,19 +1,19 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
let edgeTTS: typeof import("./tts.js").edgeTTS;
let mockTtsPromise = vi.fn<(text: string, filePath: string) => Promise<void>>();
vi.mock("node-edge-tts", () => ({
EdgeTTS: class {
ttsPromise(text: string, filePath: string) {
return mockTtsPromise(text, filePath);
}
},
}));
function createEdgeTTSDeps(ttsPromise: (text: string, filePath: string) => Promise<void>) {
return {
EdgeTTS: class {
ttsPromise(text: string, filePath: string) {
return ttsPromise(text, filePath);
}
},
};
}
const baseEdgeConfig = {
voice: "en-US-MichelleNeural",
@@ -40,17 +40,20 @@ describe("edgeTTS empty audio validation", () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
mockTtsPromise = vi.fn(async (_text: string, filePath: string) => {
const deps = createEdgeTTSDeps(async (_text: string, filePath: string) => {
writeFileSync(filePath, "");
});
await expect(
edgeTTS({
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
}),
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).rejects.toThrow("Edge TTS produced empty audio file");
});
@@ -58,17 +61,20 @@ describe("edgeTTS empty audio validation", () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
mockTtsPromise = vi.fn(async (_text: string, filePath: string) => {
const deps = createEdgeTTSDeps(async (_text: string, filePath: string) => {
writeFileSync(filePath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
});
await expect(
edgeTTS({
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
}),
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).resolves.toBeUndefined();
});
});

View File

@@ -2,6 +2,16 @@ import { statSync } from "node:fs";
import { EdgeTTS } from "node-edge-tts";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
type EdgeTTSDeps = {
EdgeTTS: new (config: ConstructorParameters<typeof EdgeTTS>[0]) => {
ttsPromise: (text: string, outputPath: string) => Promise<unknown>;
};
};
const defaultEdgeTTSDeps: EdgeTTSDeps = {
EdgeTTS,
};
export function inferEdgeExtension(outputFormat: string): string {
const normalized = normalizeLowercaseStringOrEmpty(outputFormat);
if (normalized.includes("webm")) {
@@ -19,24 +29,27 @@ export function inferEdgeExtension(outputFormat: string): string {
return ".mp3";
}
export async function edgeTTS(params: {
text: string;
outputPath: string;
config: {
voice: string;
lang: string;
outputFormat: string;
saveSubtitles: boolean;
proxy?: string;
rate?: string;
pitch?: string;
volume?: string;
timeoutMs?: number;
};
timeoutMs: number;
}): Promise<void> {
export async function edgeTTS(
params: {
text: string;
outputPath: string;
config: {
voice: string;
lang: string;
outputFormat: string;
saveSubtitles: boolean;
proxy?: string;
rate?: string;
pitch?: string;
volume?: string;
timeoutMs?: number;
};
timeoutMs: number;
},
deps: EdgeTTSDeps = defaultEdgeTTSDeps,
): Promise<void> {
const { text, outputPath, config, timeoutMs } = params;
const tts = new EdgeTTS({
const tts = new deps.EdgeTTS({
voice: config.voice,
lang: config.lang,
outputFormat: config.outputFormat,