Files
meshtastic_mqtt_server/internal/agents/sign/sign_test.go
T
kevinandClaude Fable 5 6620192322 签到检查功能增强:返回签到时间和内容
问题:
- 用户询问'我什么时候签到的'时,AI 说'系统只记录是否签到,没有时间戳'
- 实际上数据库 signs 表有完整的 sign_time 和 sign_text 字段

解决方案:
- 修改 executeCheck 方法,改用 ListSigns 查询今天的签到记录
- 返回完整的签到详情:签到时间(HH:MM:SS格式)+ 签到内容
- 未签到时仍返回简单提示

改动内容:
- executeCheck 使用 ListSigns 替代 HasSignedOnDay
- 构建 ListOptions 查询今天的签到记录(过滤 NodeID + 时间范围)
- 返回格式:'XXX 今天已经签到过了。\n签到时间:10:30:45\n签到内容:上海-Kevin-GAT562签到'
- 更新测试验证返回内容包含时间和签到文本
- 更新 mockSignStore.ListSigns 支持 NodeID 和 Limit 过滤

使用效果:
- 用户:'我今天签到了吗?' → 返回是否签到 + 时间 + 内容
- 用户:'我什么时候签到的?' → 返回签到时间:10:30:45

测试:
-  未签到场景测试通过
-  已签到场景测试通过(验证时间和内容)
-  所有签到工具测试通过
-  项目编译成功

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 21:47:16 +08:00

337 lines
8.2 KiB
Go

package sign
import (
"context"
"encoding/json"
"testing"
"time"
"meshtastic_mqtt_server/internal/agenttool"
storepkg "meshtastic_mqtt_server/internal/store"
)
// mockSignStore 是用于测试的 mock store
type mockSignStore struct {
signs []storepkg.SignRecord
nodeInfoMap map[string]*storepkg.NodeInfoRecord
}
func (m *mockSignStore) CreateSign(nodeID string, longName, shortName *string, signText string, signTime time.Time) (*storepkg.SignRecord, error) {
record := storepkg.SignRecord{
NodeID: nodeID,
LongName: longName,
ShortName: shortName,
SignText: signText,
SignTime: signTime,
}
m.signs = append(m.signs, record)
return &record, nil
}
func (m *mockSignStore) HasSignedOnDay(nodeID string, day time.Time) (bool, error) {
loc := day.Location()
if loc == nil {
loc = time.Local
}
start := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, loc)
end := start.AddDate(0, 0, 1)
for _, sign := range m.signs {
if sign.NodeID == nodeID && sign.SignTime.After(start) && sign.SignTime.Before(end) {
return true, nil
}
}
return false, nil
}
func (m *mockSignStore) GetNodeInfo(nodeID string) (*storepkg.NodeInfoRecord, error) {
return m.nodeInfoMap[nodeID], nil
}
func (m *mockSignStore) CountSigns(opts storepkg.ListOptions) (int64, error) {
count := int64(0)
for _, sign := range m.signs {
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
continue
}
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
continue
}
count++
}
return count, nil
}
func (m *mockSignStore) CountSignsByDay(opts storepkg.ListOptions) ([]storepkg.SignDayCount, error) {
dayCounts := make(map[string]int64)
for _, sign := range m.signs {
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
continue
}
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
continue
}
dateStr := sign.SignTime.Format("2006-01-02")
dayCounts[dateStr]++
}
var result []storepkg.SignDayCount
for date, count := range dayCounts {
result = append(result, storepkg.SignDayCount{Date: date, Count: count})
}
return result, nil
}
func (m *mockSignStore) ListSigns(opts storepkg.ListOptions) ([]storepkg.SignRecord, error) {
var result []storepkg.SignRecord
for _, sign := range m.signs {
// 过滤 NodeID
if opts.NodeID != "" && sign.NodeID != opts.NodeID {
continue
}
// 过滤时间范围
if opts.Since != nil && sign.SignTime.Before(*opts.Since) {
continue
}
if opts.Until != nil && sign.SignTime.After(*opts.Until) {
continue
}
result = append(result, sign)
}
// 应用 Limit
if opts.Limit > 0 && len(result) > opts.Limit {
result = result[:opts.Limit]
}
return result, nil
}
func TestSignTool_Query(t *testing.T) {
// 创建 mock store 并添加测试数据
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
yesterday := now.AddDate(0, 0, -1)
twoDaysAgo := now.AddDate(0, 0, -2)
store := &mockSignStore{
signs: []storepkg.SignRecord{
{NodeID: "node1", SignText: "上海-Alice-Device1签到", SignTime: now},
{NodeID: "node2", SignText: "北京-Bob-Device2签到", SignTime: now},
{NodeID: "node3", SignText: "深圳-Charlie-Device3签到", SignTime: yesterday},
{NodeID: "node4", SignText: "广州-David-Device4签到", SignTime: twoDaysAgo},
},
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
}
tool := &Tool{
enabled: true,
store: store,
}
tests := []struct {
name string
action string
date string
days int
expectCount int64
expectError bool
}{
{
name: "查询今天",
action: "query",
date: "2024-06-23",
days: 0,
expectCount: 2,
expectError: false,
},
{
name: "查询最近3天",
action: "query",
date: "2024-06-23",
days: 3,
expectCount: 4,
expectError: false,
},
{
name: "查询昨天",
action: "query",
date: "2024-06-22",
days: 0,
expectCount: 1,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := signParams{
Action: tt.action,
Date: tt.date,
Days: tt.days,
}
argsJSON, _ := json.Marshal(params)
runtime := agenttool.Runtime{Now: now}
result, err := tool.Execute(context.Background(), string(argsJSON), runtime)
if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !tt.expectError {
t.Logf("Query result:\n%s", result)
}
})
}
}
func TestSignTool_SignAction(t *testing.T) {
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
store := &mockSignStore{
signs: []storepkg.SignRecord{},
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
}
tool := &Tool{
enabled: true,
store: store,
}
// 测试签到功能
params := signParams{
Action: "sign",
Region: "上海闵行",
Name: "TestUser",
Device: "TestDevice",
}
argsJSON, _ := json.Marshal(params)
// 创建带节点上下文的 context
nodeCtx := agenttool.NodeContext{
NodeID: "test_node_123",
LongName: "Test Node",
ShortName: "TN",
}
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
runtime := agenttool.Runtime{Now: now}
result, err := tool.Execute(ctx, string(argsJSON), runtime)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
t.Logf("Sign result: %s", result)
// 验证签到记录已创建
if len(store.signs) != 1 {
t.Errorf("Expected 1 sign record, got %d", len(store.signs))
}
}
func TestSignTool_CheckAction(t *testing.T) {
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
// 测试场景1:今天未签到
t.Run("今天未签到", func(t *testing.T) {
store := &mockSignStore{
signs: []storepkg.SignRecord{},
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
}
tool := &Tool{
enabled: true,
store: store,
}
params := signParams{
Action: "check",
}
argsJSON, _ := json.Marshal(params)
nodeCtx := agenttool.NodeContext{
NodeID: "test_node_123",
LongName: "Test Node",
ShortName: "TN",
}
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
runtime := agenttool.Runtime{Now: now}
result, err := tool.Execute(ctx, string(argsJSON), runtime)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
t.Logf("Check result (not signed): %s", result)
if !contains(result, "还没有签到") && !contains(result, "没有签到") {
t.Errorf("Expected result to indicate not signed yet")
}
})
// 测试场景2:今天已签到
t.Run("今天已签到", func(t *testing.T) {
signTime := time.Date(2024, 6, 23, 10, 30, 45, 0, time.UTC)
store := &mockSignStore{
signs: []storepkg.SignRecord{
{
NodeID: "test_node_123",
SignText: "上海-TestUser-TestDevice签到",
SignTime: signTime,
},
},
nodeInfoMap: make(map[string]*storepkg.NodeInfoRecord),
}
tool := &Tool{
enabled: true,
store: store,
}
params := signParams{
Action: "check",
}
argsJSON, _ := json.Marshal(params)
nodeCtx := agenttool.NodeContext{
NodeID: "test_node_123",
LongName: "Test Node",
ShortName: "TN",
}
ctx := agenttool.WithNodeContext(context.Background(), nodeCtx)
runtime := agenttool.Runtime{Now: now}
result, err := tool.Execute(ctx, string(argsJSON), runtime)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
t.Logf("Check result (already signed): %s", result)
if !contains(result, "已经签到") {
t.Errorf("Expected result to indicate already signed")
}
if !contains(result, "签到时间") {
t.Errorf("Expected result to contain sign time")
}
if !contains(result, "10:30:45") {
t.Errorf("Expected result to contain the exact sign time")
}
if !contains(result, "签到内容") {
t.Errorf("Expected result to contain sign text")
}
})
}
func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
}
func containsMiddle(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}