Voice parks a routine proposal, it never accepts it (V-367)
Accepting a proposed routine gives the tick loop a standing new reason to speak. DESIGN.md § "surface caps authority" puts that at layer 3, and says voice is structurally incapable of layer 3 because a room mic is reachable by anyone in the room. The /routines button was gated at step-up; the voice path accepted outright. The two surfaces disagreed, so one of them was wrong. A spoken "да" now leaves the row 'proposed' and sends him to /routines, where the gated button is. A spoken "нет" still dismisses: declining does not move the boundary outward, so voice keeps it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CotYKycuio1GwLbYh9jfc
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/kami/maven/internal/config"
|
||||
"github.com/kami/maven/internal/delivery"
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/pattern"
|
||||
"github.com/kami/maven/internal/store"
|
||||
@@ -283,3 +284,81 @@ func TestTickProposalCooldownSpacesAnnouncements(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoiceYesDoesNotAcceptRoutine — Vikunja #367. Accepting a routine hands
|
||||
// the tick loop a standing new reason to speak, which DESIGN.md puts at layer
|
||||
// 3, and voice is structurally incapable of layer 3. A spoken "да" must park
|
||||
// the decision for the authed page, not flip the row itself.
|
||||
func TestVoiceYesDoesNotAcceptRoutine(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := refNow()
|
||||
seedRefillEvents(t, st, ctx, now, pattern.MinEvents-1)
|
||||
|
||||
h := &reactiveHandler{api: ipc.NewStoreAPI(st), dataStore: st, now: func() time.Time { return now }}
|
||||
|
||||
// The MinEvents'th event is the one that makes the pattern detectable, and
|
||||
// it goes through the voice path so the proposal is parked for a y/n.
|
||||
last := now.Add(time.Duration(pattern.MinEvents-1) * 7 * 24 * time.Hour)
|
||||
factID, err := st.WriteFact(ctx, last, store.KindSelf, "cat_water", "refill", "voice", 1.0, sql.NullInt64{})
|
||||
if err != nil {
|
||||
t.Fatalf("write fact: %v", err)
|
||||
}
|
||||
if phrase := h.detectPattern(ctx, factID, "cat_water", "refill", last); phrase == "" {
|
||||
t.Fatal("expected a parked routine proposal")
|
||||
}
|
||||
|
||||
reply, handled := h.resolveConfirm(ctx, "да")
|
||||
if !handled {
|
||||
t.Fatal("the spoken yes should be consumed by the routine confirm")
|
||||
}
|
||||
if !strings.Contains(reply, "рутин") {
|
||||
t.Fatalf("reply should send him to the routines page, got %q", reply)
|
||||
}
|
||||
|
||||
rows, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineAccepted)
|
||||
if err != nil {
|
||||
t.Fatalf("list accepted: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Fatalf("voice accepted a routine: %+v", rows)
|
||||
}
|
||||
proposed, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineProposed)
|
||||
if err != nil {
|
||||
t.Fatalf("list proposed: %v", err)
|
||||
}
|
||||
if len(proposed) != 1 {
|
||||
t.Fatalf("proposed routines = %d, want 1 (still waiting for the page)", len(proposed))
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoiceNoStillDismissesRoutine — declining does not move the boundary
|
||||
// outward, so voice keeps it. Only acceptance is gated.
|
||||
func TestVoiceNoStillDismissesRoutine(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
now := refNow()
|
||||
seedRefillEvents(t, st, ctx, now, pattern.MinEvents-1)
|
||||
|
||||
h := &reactiveHandler{api: ipc.NewStoreAPI(st), dataStore: st, now: func() time.Time { return now }}
|
||||
|
||||
last := now.Add(time.Duration(pattern.MinEvents-1) * 7 * 24 * time.Hour)
|
||||
factID, err := st.WriteFact(ctx, last, store.KindSelf, "cat_water", "refill", "voice", 1.0, sql.NullInt64{})
|
||||
if err != nil {
|
||||
t.Fatalf("write fact: %v", err)
|
||||
}
|
||||
if phrase := h.detectPattern(ctx, factID, "cat_water", "refill", last); phrase == "" {
|
||||
t.Fatal("expected a parked routine proposal")
|
||||
}
|
||||
|
||||
if _, handled := h.resolveConfirm(ctx, "нет"); !handled {
|
||||
t.Fatal("the spoken no should be consumed by the routine confirm")
|
||||
}
|
||||
rows, err := st.ListProposedRoutinesByStatus(ctx, store.RoutineDismissed)
|
||||
if err != nil {
|
||||
t.Fatalf("list dismissed: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("dismissed routines = %d, want 1", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user