Files
2026-07-13 12:22:59 +08:00

255 lines
7.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});