feat(backthink): 后台自主思考(heartbeat+事件触发 + 打扰决策 fail-closed + 主动推送活跃→IM 兜底 + 限制 + 思考记录)
This commit is contained in:
1 parent
05e23ea590
commit
6f97fded3a
13 files changed
+1968
No files matched your search
@@ -0,0 +1,137 @@
|
||||
package backthink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
)
|
||||
|
||||
// mockThinker records every Think invocation and returns a fixed conclusion. It
|
||||
// respects ctx cancellation so Stop can drain a hung pass.
|
||||
type mockThinker struct {
|
||||
mu sync.Mutex
|
||||
text string
|
||||
err error
|
||||
sleep time.Duration
|
||||
calls int
|
||||
last string
|
||||
}
|
||||
|
||||
func (m *mockThinker) Think(ctx context.Context, sessionID, trigger string) (string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.sleep > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-time.After(m.sleep):
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.calls++
|
||||
m.last = trigger
|
||||
m.mu.Unlock()
|
||||
return m.text, m.err
|
||||
}
|
||||
|
||||
func (m *mockThinker) count() int { m.mu.Lock(); defer m.mu.Unlock(); return m.calls }
|
||||
func (m *mockThinker) lastTrigger() string { m.mu.Lock(); defer m.mu.Unlock(); return m.last }
|
||||
|
||||
// mockState returns a fixed UserState.
|
||||
type mockState struct{ state UserState }
|
||||
|
||||
func (m *mockState) Current(context.Context) (UserState, error) { return m.state, nil }
|
||||
|
||||
// mockPusher records Push calls and returns a fixed result.
|
||||
type mockPusher struct {
|
||||
mu sync.Mutex
|
||||
result PushResult
|
||||
calls int
|
||||
lastMsg adapter.UnifiedMessage
|
||||
}
|
||||
|
||||
func (p *mockPusher) Push(ctx context.Context, msg adapter.UnifiedMessage) PushResult {
|
||||
p.mu.Lock()
|
||||
p.calls++
|
||||
p.lastMsg = msg
|
||||
p.mu.Unlock()
|
||||
return p.result
|
||||
}
|
||||
|
||||
func (p *mockPusher) count() int { p.mu.Lock(); defer p.mu.Unlock(); return p.calls }
|
||||
|
||||
// mockActive implements ActiveClient.
|
||||
type mockActive struct {
|
||||
mu sync.Mutex
|
||||
n int
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (m *mockActive) SendProactive(ctx context.Context, msg adapter.UnifiedMessage) (int, error) {
|
||||
m.mu.Lock()
|
||||
m.calls++
|
||||
m.mu.Unlock()
|
||||
return m.n, m.err
|
||||
}
|
||||
|
||||
// mockIMAdapter implements IMAdapter.
|
||||
type mockIMAdapter struct {
|
||||
mu sync.Mutex
|
||||
err error
|
||||
calls int
|
||||
last adapter.UnifiedMessage
|
||||
}
|
||||
|
||||
func (m *mockIMAdapter) SendProactive(ctx context.Context, msg adapter.UnifiedMessage) error {
|
||||
m.mu.Lock()
|
||||
m.calls++
|
||||
m.last = msg
|
||||
m.mu.Unlock()
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *mockIMAdapter) count() int { m.mu.Lock(); defer m.mu.Unlock(); return m.calls }
|
||||
|
||||
// mockGate implements PushGate.
|
||||
type mockGate struct{ allow bool }
|
||||
|
||||
func (g *mockGate) Allow(context.Context) bool { return g.allow }
|
||||
|
||||
// newTestDB opens an isolated in-memory SQLite handle for one test and closes it
|
||||
// on cleanup.
|
||||
func newTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
h, err := db.Open(context.Background(), ":memory:", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("open test db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = h.Close() })
|
||||
return h
|
||||
}
|
||||
|
||||
// discardLog returns a logger that writes nowhere.
|
||||
func discardLog() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
|
||||
// eventually polls cond until it is true or the deadline passes.
|
||||
func eventually(t *testing.T, timeout time.Duration, cond func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if cond() {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("condition not met within %v", timeout)
|
||||
}
|
||||
Reference in new issue
Block a user