From 044e3dad547400c443b637d36506df57b2aeee9d Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Sat, 4 Apr 2026 01:23:46 +0000 Subject: [PATCH] feat(36-02): extend nexus-settings schema with voiceMode, telegramToken, and binary paths - Export VOICE_MODES constant and VoiceMode type from nexus-settings - Export nexusSettingsSchema for testing - Add voiceMode field with default 'text' to nexusSettingsSchema - Add telegramToken optional field to nexusSettingsSchema - Add piperBinaryPath and whisperBinaryPath optional fields - Update fallback in get() to use nexusSettingsSchema.parse({}) for consistent defaults - Add 5 passing tests for nexus-settings schema in 36-voice-schema.test.ts --- server/src/__tests__/36-voice-schema.test.ts | 29 ++++++++++++++++++++ server/src/services/nexus-settings.ts | 13 +++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/36-voice-schema.test.ts b/server/src/__tests__/36-voice-schema.test.ts index 090eef06..9ebc9f1f 100644 --- a/server/src/__tests__/36-voice-schema.test.ts +++ b/server/src/__tests__/36-voice-schema.test.ts @@ -1,6 +1,7 @@ // [nexus] Schema validation tests for voiceMode field (Plan 36-02) import { describe, it, expect } from "vitest"; import { createMessageSchema } from "@paperclipai/shared/validators/chat"; +import { nexusSettingsSchema } from "../services/nexus-settings.js"; describe("createMessageSchema — voiceMode field", () => { it("parses voiceMode 'full_voice' and returns the value", () => { @@ -61,3 +62,31 @@ describe("createMessageSchema — voiceMode field", () => { expect(result.messageType).toBe("markdown"); }); }); + +describe("nexusSettingsSchema — voiceMode and telegramToken fields", () => { + it("parses settings without voiceMode and defaults to 'text'", () => { + const result = nexusSettingsSchema.parse({ mode: "both" }); + expect(result.voiceMode).toBe("text"); + }); + + it("parses settings with voiceMode 'full_voice' and preserves the value", () => { + const result = nexusSettingsSchema.parse({ mode: "both", voiceMode: "full_voice" }); + expect(result.voiceMode).toBe("full_voice"); + }); + + it("parses settings with telegramToken and preserves the value", () => { + const result = nexusSettingsSchema.parse({ mode: "both", telegramToken: "123:ABC" }); + expect(result.telegramToken).toBe("123:ABC"); + }); + + it("parses settings without telegramToken and returns undefined", () => { + const result = nexusSettingsSchema.parse({ mode: "both" }); + expect(result.telegramToken).toBeUndefined(); + }); + + it("parses existing settings without voiceMode/telegramToken without error", () => { + expect(() => + nexusSettingsSchema.parse({ mode: "personal_ai", voiceEnabled: true }) + ).not.toThrow(); + }); +}); diff --git a/server/src/services/nexus-settings.ts b/server/src/services/nexus-settings.ts index 53960da8..f90c8e1c 100644 --- a/server/src/services/nexus-settings.ts +++ b/server/src/services/nexus-settings.ts @@ -6,9 +6,16 @@ import { resolvePaperclipInstanceRoot } from "../home-paths.js"; export const NEXUS_MODES = ["personal_ai", "project_builder", "both"] as const; export type NexusMode = (typeof NEXUS_MODES)[number]; -const nexusSettingsSchema = z.object({ +export const VOICE_MODES = ["text", "voice_input", "full_voice"] as const; +export type VoiceMode = (typeof VOICE_MODES)[number]; + +export const nexusSettingsSchema = z.object({ mode: z.enum(NEXUS_MODES).default("both"), voiceEnabled: z.boolean().default(false), + voiceMode: z.enum(VOICE_MODES).default("text"), + telegramToken: z.string().optional(), + piperBinaryPath: z.string().optional(), + whisperBinaryPath: z.string().optional(), }); type NexusSettings = z.infer; @@ -26,9 +33,9 @@ export function nexusSettingsService() { if (parsed.success) { return parsed.data; } - return { mode: "both", voiceEnabled: false }; + return nexusSettingsSchema.parse({}); } catch { - return { mode: "both", voiceEnabled: false }; + return nexusSettingsSchema.parse({}); } }