The pass announces the verdict it just decided
Storing doc_lang on the document row is not enough on its own. The editor sees that row when the document is opened or saved, and the pass that decides the verdict runs after a save — so the client was always one save behind, and read-aloud is reached for precisely when she has stopped typing and no further save is coming. Heard in a browser: a Portuguese paragraph read in an American voice, twice, until another keystroke went in. /check, /voice and /collocation now answer with X-Petal-Doc-Lang. A header rather than a wider body: all three answer with a bare array of the unified pending set and every caller reads it as one, and a verdict is metadata about the pass rather than another suggestion. It reaches the app through the same handler shape onUnauthorized already uses. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
@@ -393,3 +393,56 @@ func TestTranslateStillRendersForALearnersEnglishExplanation(t *testing.T) {
|
||||
t.Fatalf("translate didn't render into the pair language:\n%s", client.lastPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassAnnouncesItsVerdict pins the header the client reads. Storing the
|
||||
// verdict on the document row is not enough on its own: the editor sees that row
|
||||
// only when the document is opened or saved, and the pass that decides the
|
||||
// verdict runs *after* a save — so the client would always be one save behind,
|
||||
// and read-aloud is reached for exactly when she has stopped typing and no
|
||||
// further save is coming. Caught in a browser: a Portuguese paragraph read in an
|
||||
// American voice, twice, until another keystroke went in.
|
||||
//
|
||||
// Asserted on both endpoints that can flip it, and on the empty-document early
|
||||
// return, which answers without ever reaching the model.
|
||||
func TestPassAnnouncesItsVerdict(t *testing.T) {
|
||||
client := &stubClient{response: `{"suggestions":[]}`}
|
||||
srv, docID, h := newDirectedServer(t, client, "pt-PT", auth.DirectionLearningEn, ptDocument)
|
||||
|
||||
for _, path := range []string{"/check", "/voice"} {
|
||||
rec := do(t, srv, http.MethodPost, "/docs/"+docID+path, "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: code=%d body=%s", path, rec.Code, rec.Body)
|
||||
}
|
||||
if got := rec.Header().Get("X-Petal-Doc-Lang"); got != docLangPair {
|
||||
t.Fatalf("%s: X-Petal-Doc-Lang = %q, want %q", path, got, docLangPair)
|
||||
}
|
||||
}
|
||||
|
||||
// An English document says so rather than saying nothing — the client has to
|
||||
// be able to hear a flip back, not just a flip away.
|
||||
if _, err := h.DB.Exec(
|
||||
`UPDATE documents SET content_text = ?, doc_lang = '' WHERE id = ?`,
|
||||
"The weather was very cold this morning. I walked to the shop and bought some bread.", docID,
|
||||
); err != nil {
|
||||
t.Fatalf("rewrite doc: %v", err)
|
||||
}
|
||||
rec := do(t, srv, http.MethodPost, "/docs/"+docID+"/check", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("check: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
if got := rec.Header().Get("X-Petal-Doc-Lang"); got != docLangEnglish {
|
||||
t.Fatalf("English document: X-Petal-Doc-Lang = %q, want %q", got, docLangEnglish)
|
||||
}
|
||||
|
||||
// The empty-document path returns before the model call, and still answers.
|
||||
if _, err := h.DB.Exec(`UPDATE documents SET content_text = '' WHERE id = ?`, docID); err != nil {
|
||||
t.Fatalf("empty doc: %v", err)
|
||||
}
|
||||
rec = do(t, srv, http.MethodPost, "/docs/"+docID+"/check", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("empty check: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
if got := rec.Header().Get("X-Petal-Doc-Lang"); got == "" {
|
||||
t.Fatal("empty document answered with no verdict header at all")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user