the spoken plan reads the clock on his wall (V-614)
FormatRU printed a plan item's At raw. An event and a reminder come off the store as UTC — a calendar fact's Ts, a reminder's FireTs — while a checklist line is built in the asking clock's zone, so one spoken sentence named two zones. This is the voice path, so it is what he actually heard; the same defect on /morning and /events was V-612. Every hour is now read in the plan's own zone, Date's, which BuildPlan sets from the asking clock. The rest-of-day path in queryDayPlan rebuilds a plan off the wire, where nothing had put the instants in that frame, so it does now. formatTime is the same bug in the same daemon: "когда я это сделал?" names a fact's Ts, and the branch that prints a wall clock printed the store's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,10 +254,17 @@ func (h *reactiveHandler) queryDayPlan(ctx context.Context, t *queryTurn) (strin
|
||||
}
|
||||
// Rebuild the pure plan so the rest-of-day rendering is the same code that
|
||||
// rendered the whole day — one formatter, one persona.
|
||||
p := morning.Plan{Date: plan.Date}
|
||||
//
|
||||
// The instants are put back in the asking clock's zone on the way in. They
|
||||
// arrive carrying whatever zone the core read them in — a calendar fact's Ts
|
||||
// and a reminder's FireTs are UTC out of the store — and FormatRU reads the
|
||||
// hours in the plan's own frame, so setting that frame here is what makes
|
||||
// the recital name his clock rather than the store's (V-614).
|
||||
zone := h.now().Location()
|
||||
p := morning.Plan{Date: plan.Date.In(zone)}
|
||||
for _, it := range plan.Items {
|
||||
p.Items = append(p.Items, morning.PlanEntry{
|
||||
At: it.At,
|
||||
At: it.At.In(zone),
|
||||
Text: it.Text,
|
||||
Kind: morning.PlanKind(it.Kind),
|
||||
Uncertain: it.Uncertain,
|
||||
|
||||
@@ -111,6 +111,37 @@ func TestQueryDayPlanRestOfDayWhenNothingIsLeft(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// "что дальше?" rebuilds the plan off the wire and renders it here, and the
|
||||
// instants on it carry the zone the core read them in — a calendar fact's Ts
|
||||
// and a reminder's FireTs are UTC out of the store. Read raw, the recital named
|
||||
// the store's clock instead of his (V-614). The asking clock is three hours off
|
||||
// whatever this machine runs in, so the assertion holds under TZ=UTC too.
|
||||
func TestQueryDayPlanRestOfDayReadsHisClock(t *testing.T) {
|
||||
_, off := time.Now().Zone()
|
||||
away := time.FixedZone("away", off+3*60*60)
|
||||
stored := time.Date(2026, 8, 3, 8, 0, 0, 0, time.UTC)
|
||||
|
||||
h := &reactiveHandler{
|
||||
api: &planAPI{plan: ipc.DayPlan{
|
||||
Date: time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC),
|
||||
Items: []ipc.DayPlanItem{{At: stored, Text: "позвонить маме", Kind: "reminder"}},
|
||||
}},
|
||||
now: func() time.Time { return time.Date(2026, 8, 3, 9, 0, 0, 0, away) },
|
||||
}
|
||||
reply, ok := h.queryDayPlan(context.Background(), &queryTurn{
|
||||
dec: router.Decision{Intent: router.IntentQuery, Utterance: "что дальше?"},
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("expected the plan source to claim it")
|
||||
}
|
||||
if want := stored.In(away).Format("15:04"); !strings.Contains(reply, want) {
|
||||
t.Errorf("the reminder is not read in his clock (%s): %q", want, reply)
|
||||
}
|
||||
if bad := stored.Format("15:04"); strings.Contains(reply, bad) {
|
||||
t.Errorf("the reminder is read in the store's zone (%s): %q", bad, reply)
|
||||
}
|
||||
}
|
||||
|
||||
// A question that is not about the plan must fall through, or the plan buries
|
||||
// the calendar listing and the weather behind it.
|
||||
func TestQueryDayPlanPassesOnEverythingElse(t *testing.T) {
|
||||
|
||||
@@ -154,6 +154,11 @@ func hasDurationWords(u string) bool {
|
||||
// Used by the query handler when answering "когда я это сделал?"-style questions.
|
||||
func formatTime(t time.Time) string {
|
||||
now := time.Now()
|
||||
// The argument is a fact's Ts, which the store hands back as UTC. Only the
|
||||
// last branch names a wall clock, and it named the store's until V-614: an
|
||||
// answer to "когда я это сделал?" read hours off, in the same sentence
|
||||
// shape the plan reads a day in.
|
||||
t = t.Local()
|
||||
if t.After(now.Add(-2*time.Minute)) && t.Before(now.Add(2*time.Minute)) {
|
||||
return "только что"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// "когда я это сделал?" answers off a fact's Ts, which the store hands back as
|
||||
// UTC, and the branch that names a wall clock printed it in whatever zone it
|
||||
// arrived in (V-614). The instant here is built three hours off this machine's
|
||||
// zone, so the assertion holds under TZ=UTC as well.
|
||||
func TestFormatTimeReadsHisClock(t *testing.T) {
|
||||
_, off := time.Now().Zone()
|
||||
away := time.FixedZone("away", off+3*60*60)
|
||||
stored := time.Now().Add(-72 * time.Hour).In(away)
|
||||
|
||||
got := formatTime(stored)
|
||||
if want := stored.Local().Format("15:04"); !strings.Contains(got, want) {
|
||||
t.Errorf("formatTime = %q, want the hour on his clock (%s)", got, want)
|
||||
}
|
||||
if bad := stored.Format("15:04"); strings.Contains(got, bad) {
|
||||
t.Errorf("formatTime = %q reads the zone the fact arrived in (%s)", got, bad)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMentionsUnknownDayReadsWordsNotStems — the defect V-581 found. The
|
||||
// weekday half of this guard was a list of stems matched with strings.Contains,
|
||||
|
||||
@@ -159,7 +159,15 @@ func (p Plan) After(now time.Time) Plan {
|
||||
// FormatRU renders the plan as maven says it. Feminine self-reference,
|
||||
// informal address, no pet names — and no exhortation: she reads the day back,
|
||||
// she does not tell him to get on with it.
|
||||
//
|
||||
// Every hour is read in the plan's own zone — Date's, which BuildPlan sets from
|
||||
// the asking clock. Printed raw, an hour read whatever zone its instant arrived
|
||||
// in: an event or a reminder comes off the store as UTC, while a checklist line
|
||||
// is built local, so one spoken sentence named two zones. This is the voice
|
||||
// path, so that is what the owner heard (V-614); the same defect on the two web
|
||||
// pages was V-612.
|
||||
func (p Plan) FormatRU() string {
|
||||
zone := p.Date.Location()
|
||||
if len(p.Items) == 0 {
|
||||
// "что дальше?" after the last item of the day. The day was not empty,
|
||||
// it is over, and saying it was empty is a false statement about a day
|
||||
@@ -171,7 +179,7 @@ func (p Plan) FormatRU() string {
|
||||
}
|
||||
parts := make([]string, len(p.Items))
|
||||
for i, it := range p.Items {
|
||||
line := fmt.Sprintf("%s — %s", it.At.Format("15:04"), it.Text)
|
||||
line := fmt.Sprintf("%s — %s", it.At.In(zone).Format("15:04"), it.Text)
|
||||
if it.Uncertain {
|
||||
line = say.S(say.PlanUncertain, map[string]string{"line": line})
|
||||
}
|
||||
|
||||
@@ -151,6 +151,37 @@ func TestPlanFormatRU(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// One spoken sentence names one clock. An event and a reminder come off the
|
||||
// store as UTC and a checklist line is built in the asking clock's zone, so the
|
||||
// raw Format printed the two halves of one sentence in two zones (V-614). The
|
||||
// zones here are three hours off whatever this machine runs in, so the test
|
||||
// tells "read in his clock" apart from "read in the zone the instant arrived
|
||||
// in" under TZ=UTC as well.
|
||||
func TestPlanFormatRUReadsEveryHourInThePlansZone(t *testing.T) {
|
||||
_, off := time.Now().Zone()
|
||||
away := time.FixedZone("away", off+3*60*60)
|
||||
|
||||
stored := time.Date(2026, 8, 3, 14, 0, 0, 0, time.UTC)
|
||||
p := Plan{
|
||||
Date: time.Date(2026, 8, 3, 0, 0, 0, 0, away),
|
||||
Items: []PlanEntry{
|
||||
{At: stored, Text: "Планёрка", Kind: PlanEvent},
|
||||
{At: planAt(time.Date(2026, 8, 3, 0, 0, 0, 0, away), 10, 30),
|
||||
Text: "утро — осталось: витамины", Kind: PlanChecklist},
|
||||
},
|
||||
}
|
||||
got := p.FormatRU()
|
||||
if want := stored.In(away).Format("15:04"); !strings.Contains(got, want) {
|
||||
t.Errorf("the event is not read in the plan's zone (%s): %q", want, got)
|
||||
}
|
||||
if bad := stored.Format("15:04"); strings.Contains(got, bad) {
|
||||
t.Errorf("the event is read in the zone it was stored in (%s): %q", bad, got)
|
||||
}
|
||||
if !strings.Contains(got, "10:30") {
|
||||
t.Errorf("the checklist line moved zone: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanAfter(t *testing.T) {
|
||||
p, now := planFixture(t)
|
||||
rest := p.After(planAt(now, 11, 0))
|
||||
|
||||
Reference in New Issue
Block a user