mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 05:32:58 +00:00
* feat(voice): add a Voice settings page with provider choice and a local model catalog Each host gets a Voice page under host settings. Dictation and voice mode each pick a provider (Local or OpenAI) and a model. Provider settings sit in one group of collapsible rows: local speech-to-text and text-to-speech list every catalog model with its size, languages and description before download, and OpenAI takes a write-only API key and an optional base URL. The local catalog grows from Parakeet/Kokoro to Whisper, SenseVoice, Dolphin, GigaAM, Parakeet Japanese, multilingual Kokoro, Kitten and Piper voices. Each entry declares its sherpa engine family, so the recognizer and TTS build the matching model config and the downloader verifies the files that family needs. Kitten v0.8 is left out: it crashes the pinned sherpa-onnx 1.12.28. Voice mode is now opt-in. The default lived only in code (true), never in config.json, so hosts that never toggled it find it off after upgrading. OpenAI speech-to-text resolved one model for both features, with voice mode's choice taking precedence, so dictation silently used voice mode's model. The resolved config now carries a model per feature; STT_MODEL still overrides both. Rows revealed under a collapsible settings row are a nested density level set by the container: tighter padding, smaller text, and xs controls, with controls taking their size from the row instead of each caller. * test(app): pre-bundle the JSX dev runtime for browser tests Compiling test JSX with the automatic runtime imports react/jsx-dev-runtime. Vite discovered it mid-run on a cold cache and reloaded the page, which failed the first browser test file to import it. * refactor(voice): keep speech provider settings at each use site Store credentials and endpoint options beside the speech feature that consumes them so saved page values cannot be shadowed by a separate provider block. Normalize existing config on load and mirror the inline shape on config reads and patches. This is the config, resolver and wire phase; the provider sheet follows separately. * fix(voice): preserve saved keys when resaving provider settings Forms send the selected provider on each save but cannot read back its API key. Merge patches for the existing provider so these saves retain credentials; replace the site only when the provider changes. * feat(voice): configure each speech use site from a provider sheet The Voice page now lists Speech-to-text, Listening and Speaking as one-line summaries. Tapping one opens a sheet with the provider choice and only that provider's settings: the local catalog (size, languages and description before download, download state per model) or the OpenAI key, base URL, model and language or voice. The sheet's form model sends only the fields the user changed, so re-saving never clears a saved key. Fields an env var decides are read-only with a "Set by" hint; the API key and base URL stay editable because a saved value wins over the env fallback. Save failures show a toast and keep the sheet open with the input. E2E daemons pin speech providers through env vars to avoid model downloads, so the browser spec covers the locked-provider path, the key fallback and the failed save; provider switching is covered by the form model tests. * refactor(voice): press settings and catalog rows through PressHighlight Settings rows that lead somewhere and local model rows now use the standard row press primitive, so native shows the finger-down flash the rest of the app's rows have. Web hover and press styling are unchanged. * fix(voice): preserve STT language for local providers Language belongs to every STT use site. Preserve legacy local language settings through config loading and saving, resolve them without provider gating, and expose them in the provider form.
577 lines
18 KiB
TypeScript
577 lines
18 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { z } from "zod";
|
|
import {
|
|
MutableDaemonConfigSchema,
|
|
MutableDaemonConfigPatchSchema,
|
|
UsageReportEntrySchema,
|
|
AgentSnapshotPayloadSchema,
|
|
AgentTimelineItemPayloadSchema,
|
|
ServerInfoStatusPayloadSchema,
|
|
SessionOutboundMessageSchema,
|
|
WSHelloMessageSchema,
|
|
WorkspaceSetupSnapshotSchema,
|
|
WorkspaceSetupProgressMessageSchema,
|
|
AgentTimelineEntryPayloadSchema,
|
|
} from "./messages.js";
|
|
|
|
test("terminal listings accept older rows and retain new per-terminal directories", () => {
|
|
const response = {
|
|
type: "list_terminals_response",
|
|
payload: {
|
|
requestId: "terminal-list",
|
|
cwd: "/workspace",
|
|
terminals: [{ id: "terminal", name: "Shell", workspaceId: "workspace" }],
|
|
},
|
|
};
|
|
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
|
|
const withDirectory = {
|
|
...response,
|
|
payload: {
|
|
...response.payload,
|
|
terminals: [{ ...response.payload.terminals[0], cwd: "/workspace/subdirectory" }],
|
|
},
|
|
};
|
|
expect(SessionOutboundMessageSchema.parse(withDirectory)).toEqual(withDirectory);
|
|
});
|
|
|
|
const LegacySubAgentToolCallSchema = z.object({
|
|
type: z.literal("tool_call"),
|
|
callId: z.string(),
|
|
name: z.string(),
|
|
status: z.enum(["running", "completed", "failed", "canceled"]),
|
|
error: z.unknown().nullable(),
|
|
detail: z.object({
|
|
type: z.literal("sub_agent"),
|
|
subAgentType: z.string().optional(),
|
|
description: z.string().optional(),
|
|
log: z.string(),
|
|
// Copied from v0.1.65-beta.3: actions was required even though the UI ignored it.
|
|
actions: z.array(
|
|
z.object({
|
|
index: z.number().int().positive(),
|
|
toolName: z.string(),
|
|
summary: z.string().optional(),
|
|
}),
|
|
),
|
|
}),
|
|
});
|
|
|
|
const LegacyAgentCapabilityFlagsSchema = z.object({
|
|
supportsStreaming: z.boolean(),
|
|
supportsSessionPersistence: z.boolean(),
|
|
supportsDynamicModes: z.boolean(),
|
|
supportsMcpServers: z.boolean(),
|
|
supportsReasoningStream: z.boolean(),
|
|
supportsToolInvocations: z.boolean(),
|
|
});
|
|
|
|
const LegacyAgentSnapshotPayloadSchema = AgentSnapshotPayloadSchema.extend({
|
|
capabilities: LegacyAgentCapabilityFlagsSchema,
|
|
});
|
|
|
|
describe("wire schema compatibility", () => {
|
|
test("hello parses with and without the project update capability", () => {
|
|
const legacy = WSHelloMessageSchema.parse({
|
|
type: "hello",
|
|
clientId: "legacy-client",
|
|
clientType: "mobile",
|
|
protocolVersion: 1,
|
|
});
|
|
const capable = WSHelloMessageSchema.parse({
|
|
type: "hello",
|
|
clientId: "capable-client",
|
|
clientType: "mobile",
|
|
protocolVersion: 1,
|
|
capabilities: { project_updates: true },
|
|
});
|
|
|
|
expect([legacy, capable]).toEqual([
|
|
{
|
|
type: "hello",
|
|
clientId: "legacy-client",
|
|
clientType: "mobile",
|
|
protocolVersion: 1,
|
|
},
|
|
{
|
|
type: "hello",
|
|
clientId: "capable-client",
|
|
clientType: "mobile",
|
|
protocolVersion: 1,
|
|
capabilities: { project_updates: true },
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("timeline replacement invalidation is opt-in and carries no timeline rows", () => {
|
|
expect(
|
|
WSHelloMessageSchema.parse({
|
|
type: "hello",
|
|
clientId: "capable-client",
|
|
clientType: "mobile",
|
|
protocolVersion: 1,
|
|
capabilities: { timeline_replacement_invalidation: true },
|
|
}).capabilities,
|
|
).toEqual({ timeline_replacement_invalidation: true });
|
|
|
|
expect(
|
|
SessionOutboundMessageSchema.parse({
|
|
type: "agent.timeline.replacement",
|
|
payload: { agentId: "agent-1", epoch: "epoch-2" },
|
|
}),
|
|
).toEqual({
|
|
type: "agent.timeline.replacement",
|
|
payload: { agentId: "agent-1", epoch: "epoch-2" },
|
|
});
|
|
});
|
|
|
|
test("server info strips unknown legacy features while accepting former turn identity", () => {
|
|
const parsed = ServerInfoStatusPayloadSchema.parse({
|
|
status: "server_info",
|
|
serverId: "legacy-server",
|
|
features: {
|
|
workspaceGithubClone: true,
|
|
agentTurnIdentity: true,
|
|
},
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
status: "server_info",
|
|
serverId: "legacy-server",
|
|
hostname: null,
|
|
version: null,
|
|
features: { agentTurnIdentity: true },
|
|
});
|
|
});
|
|
|
|
test("assistant timeline message ids are optional on the wire", () => {
|
|
expect(
|
|
AgentTimelineItemPayloadSchema.parse({
|
|
type: "assistant_message",
|
|
text: "old daemon shape",
|
|
}),
|
|
).toEqual({
|
|
type: "assistant_message",
|
|
text: "old daemon shape",
|
|
});
|
|
expect(
|
|
AgentTimelineItemPayloadSchema.parse({
|
|
type: "assistant_message",
|
|
text: "new daemon shape",
|
|
messageId: "msg-1",
|
|
}),
|
|
).toEqual({
|
|
type: "assistant_message",
|
|
text: "new daemon shape",
|
|
messageId: "msg-1",
|
|
});
|
|
});
|
|
|
|
test("task progress fields are optional on the wire", () => {
|
|
expect(
|
|
AgentTimelineItemPayloadSchema.parse({
|
|
type: "todo",
|
|
items: [{ text: "Legacy task", completed: false }],
|
|
}),
|
|
).toEqual({ type: "todo", items: [{ text: "Legacy task", completed: false }] });
|
|
expect(
|
|
AgentTimelineItemPayloadSchema.parse({
|
|
type: "todo",
|
|
items: [
|
|
{
|
|
id: "task-1",
|
|
text: "Current task",
|
|
activeForm: "Working on current task",
|
|
status: "in_progress",
|
|
completed: false,
|
|
},
|
|
],
|
|
}),
|
|
).toEqual({
|
|
type: "todo",
|
|
items: [
|
|
{
|
|
id: "task-1",
|
|
text: "Current task",
|
|
activeForm: "Working on current task",
|
|
status: "in_progress",
|
|
completed: false,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => {
|
|
const parsed = LegacySubAgentToolCallSchema.parse({
|
|
type: "tool_call",
|
|
callId: "call-sub-agent-1",
|
|
name: "Task",
|
|
status: "completed",
|
|
error: null,
|
|
detail: {
|
|
type: "sub_agent",
|
|
subAgentType: "Explore",
|
|
description: "Inspect repository structure",
|
|
childSessionId: "child-session-1",
|
|
log: "[Read] README.md",
|
|
actions: [],
|
|
},
|
|
});
|
|
|
|
expect(parsed.detail.actions).toEqual([]);
|
|
});
|
|
|
|
test("old clients parse agent snapshots with rewind capabilities", () => {
|
|
const parsed = LegacyAgentSnapshotPayloadSchema.parse({
|
|
id: "agent-1",
|
|
provider: "claude",
|
|
cwd: "/tmp/project",
|
|
model: null,
|
|
thinkingOptionId: null,
|
|
effectiveThinkingOptionId: null,
|
|
createdAt: "2026-05-23T00:00:00.000Z",
|
|
updatedAt: "2026-05-23T00:00:00.000Z",
|
|
lastUserMessageAt: null,
|
|
status: "idle",
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
supportsRewindConversation: true,
|
|
supportsRewindFiles: true,
|
|
supportsRewindBoth: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
title: null,
|
|
labels: {},
|
|
});
|
|
|
|
expect(parsed.capabilities).toEqual({
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
});
|
|
});
|
|
|
|
test("new clients parse agent snapshots without rewind capabilities", () => {
|
|
const parsed = AgentSnapshotPayloadSchema.parse({
|
|
id: "agent-1",
|
|
provider: "claude",
|
|
cwd: "/tmp/project",
|
|
model: null,
|
|
thinkingOptionId: null,
|
|
effectiveThinkingOptionId: null,
|
|
createdAt: "2026-05-23T00:00:00.000Z",
|
|
updatedAt: "2026-05-23T00:00:00.000Z",
|
|
lastUserMessageAt: null,
|
|
status: "idle",
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
title: null,
|
|
labels: {},
|
|
});
|
|
|
|
expect(parsed.capabilities.supportsRewindConversation).toBe(false);
|
|
expect(parsed.capabilities.supportsRewindFiles).toBe(false);
|
|
expect(parsed.capabilities.supportsRewindBoth).toBe(false);
|
|
});
|
|
|
|
test("notification timeline items parse their level and message", () => {
|
|
expect(
|
|
AgentTimelineItemPayloadSchema.parse({
|
|
type: "notification",
|
|
level: "warning",
|
|
message: "Command blocked by user",
|
|
}),
|
|
).toEqual({
|
|
type: "notification",
|
|
level: "warning",
|
|
message: "Command blocked by user",
|
|
});
|
|
});
|
|
});
|
|
|
|
test("0.8 timeline and setup capabilities remain optional in the hello", () => {
|
|
const hello = { type: "hello", clientId: "compat", clientType: "mobile", protocolVersion: 1 };
|
|
expect(WSHelloMessageSchema.safeParse(hello).success).toBe(true);
|
|
expect(
|
|
WSHelloMessageSchema.parse({
|
|
...hello,
|
|
capabilities: { plugin_timeline_items: true, workspace_setup_blocked: true },
|
|
}).capabilities,
|
|
).toEqual({ plugin_timeline_items: true, workspace_setup_blocked: true });
|
|
});
|
|
|
|
test("plugin rows and identity merges require a capable receiver", () => {
|
|
const item = {
|
|
type: "plugin",
|
|
id: "task",
|
|
pluginId: "tasks",
|
|
kind: "tasks",
|
|
version: 1,
|
|
data: { text: "Working" },
|
|
};
|
|
expect(AgentTimelineItemPayloadSchema.parse(item)).toEqual(item);
|
|
const legacyItems = z.object({
|
|
type: z.enum([
|
|
"user_message",
|
|
"assistant_message",
|
|
"reasoning",
|
|
"tool_call",
|
|
"todo",
|
|
"error",
|
|
"notification",
|
|
"compaction",
|
|
]),
|
|
});
|
|
expect(legacyItems.safeParse(item).success).toBe(false);
|
|
const entry = {
|
|
provider: "codex",
|
|
item,
|
|
timestamp: "2026-09-07T00:00:00.000Z",
|
|
seqStart: 1,
|
|
seqEnd: 2,
|
|
sourceSeqRanges: [{ startSeq: 1, endSeq: 2 }],
|
|
collapsed: ["identity"],
|
|
};
|
|
expect(AgentTimelineEntryPayloadSchema.parse(entry)).toEqual(entry);
|
|
const legacyCollapsed = z.array(z.enum(["assistant_merge", "reasoning_merge", "tool_lifecycle"]));
|
|
expect(legacyCollapsed.safeParse(entry.collapsed).success).toBe(false);
|
|
});
|
|
|
|
test("blocked setup preserves the legacy failed shape and optional provenance", () => {
|
|
const snapshot = {
|
|
status: "blocked",
|
|
error: null,
|
|
detail: {
|
|
type: "worktree_setup",
|
|
worktreePath: "/workspace",
|
|
branchName: "fork",
|
|
log: "",
|
|
commands: [],
|
|
},
|
|
blockedSource: {
|
|
kind: "change_request",
|
|
forge: "github",
|
|
number: 42,
|
|
headRepository: "contributor/project",
|
|
},
|
|
};
|
|
expect(WorkspaceSetupSnapshotSchema.parse(snapshot)).toEqual(snapshot);
|
|
const legacyStatus = z.enum(["running", "completed", "failed"]);
|
|
const legacySnapshot = WorkspaceSetupSnapshotSchema.omit({ blockedSource: true }).extend({
|
|
status: legacyStatus,
|
|
});
|
|
expect(legacySnapshot.safeParse(snapshot).success).toBe(false);
|
|
const failed = { ...snapshot, status: "failed", error: "Update Paseo to review and run setup." };
|
|
expect(legacySnapshot.safeParse(failed).success).toBe(true);
|
|
const progress = {
|
|
type: "workspace_setup_progress",
|
|
payload: { ...failed, workspaceId: "workspace" },
|
|
};
|
|
expect(WorkspaceSetupProgressMessageSchema.parse(progress)).toEqual(progress);
|
|
expect(WorkspaceSetupSnapshotSchema.parse(legacySnapshot.parse(failed))).toEqual(
|
|
legacySnapshot.parse(failed),
|
|
);
|
|
});
|
|
|
|
test("usage login errors are additive and older reports still parse", () => {
|
|
const entry = {
|
|
id: "codex:account",
|
|
account: {},
|
|
fetchedAt: "2026-10-05T00:00:00.000Z",
|
|
sourceId: "codex",
|
|
sourceLabel: "Codex",
|
|
report: { status: "error", error: "Usage API returned 500" },
|
|
};
|
|
const legacy = z.object({
|
|
type: z.literal("usage.list_reports.update"),
|
|
payload: z.object({
|
|
requestId: z.string(),
|
|
report: z.object({
|
|
id: z.string(),
|
|
account: z.object({ label: z.string().optional() }),
|
|
fetchedAt: z.string(),
|
|
sourceId: z.string(),
|
|
sourceLabel: z.string(),
|
|
icon: z.string().optional(),
|
|
report: z.discriminatedUnion("status", [
|
|
z.object({ status: z.literal("available"), windows: z.array(z.unknown()) }),
|
|
z.object({ status: z.literal("unavailable"), problem: z.unknown() }),
|
|
z.object({ status: z.literal("error"), error: z.string() }),
|
|
]),
|
|
}),
|
|
}),
|
|
});
|
|
const oldMessage = {
|
|
type: "usage.list_reports.update",
|
|
payload: { requestId: "usage", report: entry },
|
|
};
|
|
const newMessage = {
|
|
...oldMessage,
|
|
payload: {
|
|
...oldMessage.payload,
|
|
report: {
|
|
...entry,
|
|
loginErrors: [{ harness: "Codex", report: entry.report }],
|
|
},
|
|
},
|
|
};
|
|
expect(SessionOutboundMessageSchema.parse(oldMessage)).toEqual(oldMessage);
|
|
expect(SessionOutboundMessageSchema.parse(newMessage)).toEqual(newMessage);
|
|
expect(legacy.parse(newMessage)).toEqual(oldMessage);
|
|
});
|
|
|
|
test("usage entries accept older failures and additive successful snapshots", () => {
|
|
const oldEntry = {
|
|
id: "source:account",
|
|
sourceId: "source",
|
|
sourceLabel: "Source",
|
|
account: {},
|
|
fetchedAt: "2026-10-17T10:06:00Z",
|
|
report: { status: "error", error: "Rate limited" },
|
|
};
|
|
expect(UsageReportEntrySchema.parse(oldEntry)).toEqual(oldEntry);
|
|
const retained = {
|
|
...oldEntry,
|
|
lastSuccessful: {
|
|
fetchedAt: "2026-10-17T10:00:00Z",
|
|
report: { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct: 42 }] },
|
|
},
|
|
};
|
|
expect(UsageReportEntrySchema.parse(retained)).toEqual(retained);
|
|
const available = {
|
|
...oldEntry,
|
|
report: retained.lastSuccessful.report,
|
|
successfulAt: "2026-10-17T10:00:15Z",
|
|
};
|
|
expect(UsageReportEntrySchema.parse(available)).toEqual(available);
|
|
expect(
|
|
UsageReportEntrySchema.omit({ lastSuccessful: true, successfulAt: true }).parse(available),
|
|
).toEqual({ ...oldEntry, report: available.report });
|
|
// An older client's entry shape strips the additive snapshot and still sees the failure.
|
|
expect(UsageReportEntrySchema.omit({ lastSuccessful: true }).parse(retained)).toEqual(oldEntry);
|
|
expect(
|
|
UsageReportEntrySchema.safeParse({
|
|
...oldEntry,
|
|
lastSuccessful: { fetchedAt: oldEntry.fetchedAt, report: oldEntry.report },
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
// Frozen setup detail shape from v0.8.0: log is required, command output is optional.
|
|
const ReleasedSetupDetailSchema = z.object({
|
|
type: z.literal("worktree_setup"),
|
|
worktreePath: z.string(),
|
|
branchName: z.string(),
|
|
log: z.string(),
|
|
commands: z.array(
|
|
z.object({
|
|
index: z.number().int().positive(),
|
|
command: z.string(),
|
|
cwd: z.string(),
|
|
log: z.string().optional().default(""),
|
|
status: z.enum(["running", "completed", "failed"]),
|
|
exitCode: z.number().nullable(),
|
|
durationMs: z.number().nonnegative().optional(),
|
|
}),
|
|
),
|
|
truncated: z.boolean().optional(),
|
|
});
|
|
|
|
test("released setup clients parse empty combined logs and retain command output", () => {
|
|
const detail = {
|
|
type: "worktree_setup",
|
|
worktreePath: "/workspace",
|
|
branchName: "feature",
|
|
log: "",
|
|
commands: [
|
|
{
|
|
index: 1,
|
|
command: "echo ready",
|
|
cwd: "/workspace",
|
|
log: "ready\n",
|
|
status: "completed",
|
|
exitCode: 0,
|
|
},
|
|
],
|
|
};
|
|
const parsed = ReleasedSetupDetailSchema.parse(JSON.parse(JSON.stringify(detail)));
|
|
expect(parsed).toEqual(detail);
|
|
expect(parsed.commands[0]?.log).toBe("ready\n");
|
|
const { log: _log, ...withoutLegacyField } = detail;
|
|
expect(ReleasedSetupDetailSchema.safeParse(withoutLegacyField).success).toBe(false);
|
|
expect(
|
|
WorkspaceSetupSnapshotSchema.parse({
|
|
status: "completed",
|
|
detail: { ...detail, log: "old daemon combined output" },
|
|
error: null,
|
|
}).detail.log,
|
|
).toBe("old daemon combined output");
|
|
});
|
|
|
|
test("local STT language survives wire reads and patches", () => {
|
|
const patch = {
|
|
features: {
|
|
dictation: { stt: { provider: "local", language: "fr" } },
|
|
voiceMode: { stt: { provider: "local", language: null } },
|
|
},
|
|
};
|
|
expect(MutableDaemonConfigPatchSchema.parse(patch)).toEqual(patch);
|
|
});
|
|
|
|
test("speech config reads remain compatible with clients before features and future providers", () => {
|
|
const oldSchema = MutableDaemonConfigSchema.omit({ features: true, envOverrides: true });
|
|
const oldRead = oldSchema.parse({
|
|
providers: {},
|
|
relay: { enabled: false },
|
|
mcp: { enabled: true, injectIntoAgents: true },
|
|
});
|
|
const read = {
|
|
...oldRead,
|
|
features: {
|
|
dictation: { enabled: true, stt: { provider: "local", language: "fr" } },
|
|
voiceMode: {
|
|
enabled: false,
|
|
stt: { provider: "future", model: "future-model", region: "eu" },
|
|
tts: { provider: "openai", hasApiKey: false },
|
|
},
|
|
},
|
|
envOverrides: { "features.voiceMode.stt.provider": "PASEO_VOICE_STT_PROVIDER" },
|
|
};
|
|
expect(oldSchema.parse(read)).toEqual(read);
|
|
expect(MutableDaemonConfigSchema.parse(oldRead)).toEqual(oldRead);
|
|
expect(MutableDaemonConfigSchema.parse(read).features?.dictation.stt).toEqual(
|
|
read.features.dictation.stt,
|
|
);
|
|
expect(MutableDaemonConfigSchema.parse(read).features?.voiceMode.stt).toEqual(
|
|
read.features.voiceMode.stt,
|
|
);
|
|
const oldPatchSchema = MutableDaemonConfigPatchSchema.omit({ features: true });
|
|
const oldPatch = oldPatchSchema.parse({ relay: { enabled: true } });
|
|
expect(MutableDaemonConfigPatchSchema.parse(oldPatch)).toEqual(oldPatch);
|
|
expect(
|
|
MutableDaemonConfigPatchSchema.parse({
|
|
features: { voiceMode: { stt: { provider: "future", region: "eu" } } },
|
|
}).features?.voiceMode?.stt,
|
|
).toEqual({ provider: "future", region: "eu" });
|
|
});
|