255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||
import { Settings, settings } from "../src/config/settings";
|
||
import * as downloader from "../src/stt/downloader";
|
||
import * as recorder from "../src/stt/recorder";
|
||
import { STTController } from "../src/stt/stt-controller";
|
||
import { evaluateSubmitTrigger, type SttSubmitTrigger } from "../src/stt/submit-trigger";
|
||
import * as transcriber from "../src/stt/transcriber";
|
||
import { beginSettingsTest, restoreSettingsTestState, type SettingsTestState } from "./helpers/settings-test-state";
|
||
|
||
describe("STT Submit Trigger Evaluation", () => {
|
||
describe("never trigger", () => {
|
||
it("should never submit", () => {
|
||
expect(evaluateSubmitTrigger("hello world", "never")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("submit", "never")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("", "never")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("release trigger", () => {
|
||
it("should only submit if utterance has 2+ words", () => {
|
||
expect(evaluateSubmitTrigger("hello", "release")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger(" hello ", "release")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello world", "release")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello world!", "release")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("one two three", "release")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("", "release")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("release-complete trigger", () => {
|
||
it("should submit only if utterance ends with terminal punctuation", () => {
|
||
expect(evaluateSubmitTrigger("hello", "release-complete")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello world", "release-complete")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello.", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello?", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello!", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello...", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
// Full-width punctuation
|
||
expect(evaluateSubmitTrigger("hello。", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello?", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello!", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello…", "release-complete")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("", "release-complete")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("say-submit trigger", () => {
|
||
it("should submit and trim trailing word when last word contains submit", () => {
|
||
// Single word
|
||
expect(evaluateSubmitTrigger("submit", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 6,
|
||
});
|
||
expect(evaluateSubmitTrigger("SUBMIT", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 6,
|
||
});
|
||
expect(evaluateSubmitTrigger("submit!", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 7,
|
||
});
|
||
|
||
// Multi word
|
||
expect(evaluateSubmitTrigger("please submit", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 7, // " submit" has length 7
|
||
});
|
||
expect(evaluateSubmitTrigger("please submit.", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 8, // " submit." has length 8
|
||
});
|
||
expect(evaluateSubmitTrigger("please submit?", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 8,
|
||
});
|
||
expect(evaluateSubmitTrigger("please submit ", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 9, // " submit " has length 9
|
||
});
|
||
|
||
// Word containing submit
|
||
expect(evaluateSubmitTrigger("please autosubmit", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 11, // " autosubmit" has length 11
|
||
});
|
||
expect(evaluateSubmitTrigger("please submitting", "say-submit")).toEqual({
|
||
submit: true,
|
||
trimTrailing: 11,
|
||
});
|
||
|
||
// Negative cases
|
||
expect(evaluateSubmitTrigger("submit please", "say-submit")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("hello", "say-submit")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
expect(evaluateSubmitTrigger("", "say-submit")).toEqual({
|
||
submit: false,
|
||
trimTrailing: 0,
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("STTController submit trigger integration", () => {
|
||
let state: SettingsTestState | undefined;
|
||
let controller: STTController | undefined;
|
||
|
||
function makeEditor() {
|
||
return {
|
||
insertText: vi.fn(),
|
||
setVolatileText: vi.fn(),
|
||
clearVolatileText: vi.fn(),
|
||
commitVolatileText: vi.fn(),
|
||
submit: vi.fn(),
|
||
deleteBeforeCursor: vi.fn(),
|
||
};
|
||
}
|
||
|
||
function makeOptions() {
|
||
return {
|
||
showWarning: vi.fn(),
|
||
showStatus: vi.fn(),
|
||
onStateChange: vi.fn(),
|
||
requestRender: vi.fn(),
|
||
};
|
||
}
|
||
|
||
async function transcribeBatch(transcript: string, trigger: SttSubmitTrigger) {
|
||
settings.set("stt.submitTrigger", trigger);
|
||
vi.spyOn(transcriber, "transcribe").mockResolvedValue(transcript);
|
||
const editor = makeEditor();
|
||
const options = makeOptions();
|
||
controller = new STTController();
|
||
|
||
await controller.toggle(editor, options);
|
||
expect(controller.state).toBe("recording");
|
||
await controller.toggle(editor, options);
|
||
expect(controller.state).toBe("idle");
|
||
|
||
return { editor, options };
|
||
}
|
||
|
||
beforeEach(async () => {
|
||
state = beginSettingsTest();
|
||
await Settings.init({ inMemory: true });
|
||
settings.set("stt.modelName", "fast");
|
||
settings.set("stt.submitTrigger", "never");
|
||
vi.spyOn(downloader, "isSttModelCached").mockResolvedValue(true);
|
||
vi.spyOn(downloader, "downloadSttModel").mockResolvedValue(undefined);
|
||
vi.spyOn(recorder, "ensureRecorder").mockResolvedValue({ tool: "sox", bin: "sox" });
|
||
vi.spyOn(recorder, "detectRecorder").mockReturnValue({ tool: "powershell", bin: "powershell" });
|
||
vi.spyOn(recorder, "startRecording").mockResolvedValue({ stop: vi.fn().mockResolvedValue(undefined) });
|
||
vi.spyOn(recorder, "verifyRecordingFile").mockResolvedValue(1);
|
||
});
|
||
|
||
afterEach(() => {
|
||
controller?.dispose();
|
||
controller = undefined;
|
||
vi.restoreAllMocks();
|
||
restoreSettingsTestState(state);
|
||
});
|
||
|
||
it("submits batch dictation on release when the transcript has at least two words", async () => {
|
||
const { editor } = await transcribeBatch("hello world", "release");
|
||
|
||
expect(editor.insertText).toHaveBeenCalledWith("hello world");
|
||
expect(editor.submit).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it("does not submit one-word batch dictation on release", async () => {
|
||
const { editor } = await transcribeBatch("hello", "release");
|
||
|
||
expect(editor.insertText).toHaveBeenCalledWith("hello");
|
||
expect(editor.submit).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("strips the spoken submit command before submitting batch dictation", async () => {
|
||
const { editor } = await transcribeBatch("please review this submit.", "say-submit");
|
||
|
||
expect(editor.insertText).toHaveBeenCalledWith("please review this");
|
||
expect(editor.submit).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it("submits the existing draft when the batch transcript only says submit", async () => {
|
||
const { editor } = await transcribeBatch("submit", "say-submit");
|
||
|
||
expect(editor.insertText).not.toHaveBeenCalled();
|
||
expect(editor.submit).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|