Phase 8 follow-up: resolve the child named in the question
The Hub forwards `person` equal to the asker (`PersonOrDefault`), so the spoken question "what does Harry have on today at school?" arrives with person "sam" and returned "no data for that person" (found by the Hub agent on AUD-116). resolveChild now falls back to the question text when the requested person is not one of this project's children: whole-word, case-insensitive match against the child names this service already publishes in /capabilities. An explicit person still wins, two named children still return answer:null, and the permission check still runs on the resolved child. No model, no free text. Live end-to-end through the Hub, with no person field: POST http://192.168.20.13:8788/ask {"text":"what does Harry have on today at school?","asker":"sam","source":"console"} 200 {"status":"ok", "answer":"Harry has 8 school events today from 8:55am to 6:00pm and 6 overdue tasks.", "route":{"kind":"project","project":"compass_nhs"}} Image 0.6.1-phase8; docs/phase-8-hub-notes.md records the fallback and the end-to-end transcript. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -75,8 +75,12 @@ func harryData() *fakeReader {
|
||||
{ID: 9, ChildID: 1, Type: "task", Subject: "11MGMA MATHS", Title: "SAC 4: Networks",
|
||||
Body: "Status: Pending", DueAt: at(2026, 10, 19, 16, 0)},
|
||||
{ID: 10, ChildID: 1, Type: "message", Title: "Health Information", DueAt: at(2026, 10, 13, 9, 0)},
|
||||
}},
|
||||
scans: map[int64]store.ScanStatus{1: okScan(testNow.Add(-4 * time.Hour))},
|
||||
},
|
||||
2: {
|
||||
{ID: 20, ChildID: 2, Type: "event", Title: "9:00: 10ENGA - B2 - SMI", DueAt: at(2026, 10, 15, 9, 0)},
|
||||
},
|
||||
},
|
||||
scans: map[int64]store.ScanStatus{1: okScan(testNow.Add(-4 * time.Hour)), 2: okScan(testNow.Add(-4 * time.Hour))},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +237,72 @@ func TestEmptyQuestionCannotAnswer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestChildNamedInQuestionResolvesThePerson covers the Hub's actual call shape:
|
||||
// the Hub sends `person` equal to the asker, so the child's name is only in the
|
||||
// question. The literal phase question must still answer.
|
||||
func TestChildNamedInQuestionResolvesThePerson(t *testing.T) {
|
||||
a := testAnswerer(harryData())
|
||||
resp := a.Answer(context.Background(), AskRequest{
|
||||
Question: "what does Harry have on today at school?",
|
||||
Asker: "sam",
|
||||
Person: "sam", // what the Hub forwards today (person defaults to asker)
|
||||
Source: "console",
|
||||
})
|
||||
if resp.Answer == nil {
|
||||
t.Fatalf("answer = nil, reason = %q", resp.Reason)
|
||||
}
|
||||
if resp.Data["child"] != "Harry" {
|
||||
t.Errorf("data.child = %v, want Harry", resp.Data["child"])
|
||||
}
|
||||
if !strings.Contains(*resp.Answer, "school event") {
|
||||
t.Errorf("answer = %q, want the lesson count", *resp.Answer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExplicitChildWinsOverQuestionName: a caller that names a child wins, even
|
||||
// if the question text mentions another one.
|
||||
func TestExplicitChildWinsOverQuestionName(t *testing.T) {
|
||||
a := testAnswerer(harryData())
|
||||
resp := a.Answer(context.Background(), AskRequest{
|
||||
Question: "what does Harry have on today?",
|
||||
Asker: "sam",
|
||||
Person: "finn",
|
||||
})
|
||||
if resp.Answer == nil {
|
||||
t.Fatalf("answer = nil, reason = %q", resp.Reason)
|
||||
}
|
||||
if resp.Data["child"] != "Finn" {
|
||||
t.Errorf("data.child = %v, want Finn", resp.Data["child"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestTwoChildrenNamedCannotAnswer: the contract answers for one person at
|
||||
// most, so an ambiguous question is refused rather than guessed.
|
||||
func TestTwoChildrenNamedCannotAnswer(t *testing.T) {
|
||||
a := testAnswerer(harryData())
|
||||
resp := a.Answer(context.Background(), AskRequest{
|
||||
Question: "what do Harry and Finn have on today?",
|
||||
Asker: "sam",
|
||||
Person: "sam",
|
||||
})
|
||||
if resp.Answer != nil {
|
||||
t.Fatalf("answer = %q, want nil for an ambiguous question", *resp.Answer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNameIsNotMatchedInsideAWord keeps the fallback to whole words.
|
||||
func TestNameIsNotMatchedInsideAWord(t *testing.T) {
|
||||
a := testAnswerer(harryData())
|
||||
resp := a.Answer(context.Background(), AskRequest{
|
||||
Question: "what does harrysaurus have on today?",
|
||||
Asker: "sam",
|
||||
Person: "sam",
|
||||
})
|
||||
if resp.Answer != nil {
|
||||
t.Fatalf("answer = %q, want nil (no whole-word child name)", *resp.Answer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilitiesNameMatchesRouting(t *testing.T) {
|
||||
caps := DefaultCapabilities()
|
||||
if caps.Project != "compass_nhs" {
|
||||
|
||||
Reference in New Issue
Block a user