d3cee71bd2023a259eb7784a15cd5ef56a3c78cf

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

test: close coverage gaps for contracts, validation, prompts, romanization, adapter errors

Diff

 1diff --git a/internal/adapters/speech_test.go b/internal/adapters/speech_test.go
 2index e3a33901ad09f0e27b2f98b11188c46ddb45c246..cc87cc739c4c402f292249cecc929169123bc1c3 100644
 3--- a/internal/adapters/speech_test.go
 4+++ b/internal/adapters/speech_test.go
 5@@ -8,6 +8,7 @@ import (
 6 	"testing"
 7 	"time"
 8 
 9+	"japanese/internal/stt"
10 	"japanese/internal/tts"
11 )
12 
13@@ -45,3 +46,55 @@ func TestSpeechOutputSpeakErrorDoesNotFireOnDone(t *testing.T) {
14 	case <-time.After(50 * time.Millisecond):
15 	}
16 }
17+
18+func mustRomanizer(t *testing.T) *stt.Romanizer {
19+	t.Helper()
20+	r, err := stt.NewRomanizer()
21+	if err != nil {
22+		t.Fatalf("NewRomanizer: %v", err)
23+	}
24+	return r
25+}
26+
27+func TestSpeechInputBeginSurfacesRecorderFailure(t *testing.T) {
28+	in := &SpeechInput{
29+		Recorder: stt.NewRecorder("jp-definitely-not-a-real-command-xyz", 10*time.Second),
30+		Whisper:  stt.NewWhisperClient("http://127.0.0.1:0/inference", "", nil),
31+		Romanize: mustRomanizer(t),
32+	}
33+	err := in.Begin(context.Background())
34+	if err == nil || !strings.Contains(err.Error(), "mic:") {
35+		t.Fatalf("expected a mic error, got %v", err)
36+	}
37+}
38+
39+func TestSpeechInputEndWithoutBegin(t *testing.T) {
40+	in := &SpeechInput{
41+		Recorder: stt.NewRecorder("sleep 5", 10*time.Second),
42+		Whisper:  stt.NewWhisperClient("http://127.0.0.1:0/inference", "", nil),
43+		Romanize: mustRomanizer(t),
44+	}
45+	if _, err := in.End(context.Background()); err == nil || !strings.Contains(err.Error(), "recording not started") {
46+		t.Fatalf("expected a not-started error, got %v", err)
47+	}
48+}
49+
50+func TestSpeechInputEndSurfacesSTTErrorNamingURL(t *testing.T) {
51+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52+		http.Error(w, "boom", http.StatusInternalServerError)
53+	}))
54+	defer srv.Close()
55+
56+	in := &SpeechInput{
57+		Recorder: stt.NewRecorder("sleep 5", 10*time.Second),
58+		Whisper:  stt.NewWhisperClient(srv.URL, "", srv.Client()),
59+		Romanize: mustRomanizer(t),
60+	}
61+	if err := in.Begin(context.Background()); err != nil {
62+		t.Fatalf("Begin: %v", err)
63+	}
64+	_, err := in.End(context.Background())
65+	if err == nil || !strings.Contains(err.Error(), srv.URL) {
66+		t.Fatalf("expected STT error naming URL %q, got %v", srv.URL, err)
67+	}
68+}
69diff --git a/internal/llm/contract_test.go b/internal/llm/contract_test.go
70index 427ebc665037c4d9f6deffb2e86bec69d825dae2..90c7b0c393e61c70bc9ef0c6600fa1e660786a55 100644
71--- a/internal/llm/contract_test.go
72+++ b/internal/llm/contract_test.go
73@@ -141,3 +141,24 @@ func TestPromptTemplatesHaveNoGameOrGradingWords(t *testing.T) {
74 		}
75 	}
76 }
77+
78+func TestContractErrorMessageNamesFieldAndKind(t *testing.T) {
79+	cases := []struct {
80+		kind  ContractErrorKind
81+		field string
82+		want  string
83+	}{
84+		{MissingField, "KANA", "missing"},
85+		{DuplicateField, "ENGLISH", "duplicate"},
86+		{EmptyValue, "ROMAJI", "empty value"},
87+		{InvalidFormat, "SCORE", "invalid format"},
88+		{OutOfRange, "SCORE", "out of range"},
89+	}
90+	for _, c := range cases {
91+		e := &ContractError{Kind: c.kind, Field: c.field}
92+		got := e.Error()
93+		if !strings.Contains(got, c.field) || !strings.Contains(got, c.want) {
94+			t.Errorf("Error(%v, %s) = %q; want it to name the field and %q", c.kind, c.field, got, c.want)
95+		}
96+	}
97+}