mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
bc374fe8cd
* refactor(attachment): multimodal attachment refactor with snapshot schema and storage layer - Add snapshot schema migration (0008) and update init/versions/snapshots - Add internal/attachment and internal/channel normalize for unified attachment handling - Move containerfs provider from internal/media to internal/storage - Update agent types, channel adapters (Telegram/Feishu), inbound and handlers - Add containerd snapshot lineage and local_channel tests - Regenerate sqlc, swagger and SDK * refactor(media): content-addressed asset system with unified naming - Replace asset_id foreign key with content_hash as sole identifier for bot_history_message_assets (pure soft-link model) - Remove mime, size_bytes, storage_key from DB; derive at read time via media.Resolve from actual storage - Merge migrations 0008/0009 into single 0008; keep 0001 as canonical schema - Add Docker initdb script for deterministic migration execution order - Fix cross-channel real-time image display (Telegram → WebUI SSE) - Fix message disappearing on refresh (null assets fallback) - Fix file icon instead of image preview (mime derivation from storage) - Unify AssetID → ContentHash naming across Go, Agent, and Frontend - Change storage key prefix from 4-char to 2-char for directory sharding - Add server-entrypoint.sh for Docker deployment migration handling * refactor(infra): embedded migrations, Docker simplification, and config consolidation - Embed SQL migrations into Go binary, removing shell-based migration scripts - Consolidate config files into conf/ directory (app.example.toml, app.docker.toml, app.dev.toml) - Simplify Docker setup: remove initdb.d scripts, streamline nginx config and entrypoint - Remove legacy CLI, feishu-echo commands, and obsolete incremental migration files - Update install script and docs to require sudo for one-click install - Add mise tasks for dev environment orchestration * chore: recover migrations --------- Co-authored-by: Acbox <acbox0328@gmail.com>
151 lines
3.7 KiB
Go
151 lines
3.7 KiB
Go
package channel
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
type recordingObserver struct {
|
|
mu sync.Mutex
|
|
events []observedEvent
|
|
}
|
|
|
|
type observedEvent struct {
|
|
BotID string
|
|
Source ChannelType
|
|
Event StreamEvent
|
|
}
|
|
|
|
func (r *recordingObserver) OnStreamEvent(_ context.Context, botID string, source ChannelType, event StreamEvent) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.events = append(r.events, observedEvent{BotID: botID, Source: source, Event: event})
|
|
}
|
|
|
|
func (r *recordingObserver) recorded() []observedEvent {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := make([]observedEvent, len(r.events))
|
|
copy(cp, r.events)
|
|
return cp
|
|
}
|
|
|
|
// stubStream records pushed events.
|
|
type stubStream struct {
|
|
mu sync.Mutex
|
|
events []StreamEvent
|
|
closed bool
|
|
}
|
|
|
|
func (s *stubStream) Push(_ context.Context, event StreamEvent) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.events = append(s.events, event)
|
|
return nil
|
|
}
|
|
|
|
func (s *stubStream) Close(_ context.Context) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.closed = true
|
|
return nil
|
|
}
|
|
|
|
func TestNewTeeStream_NilObserver(t *testing.T) {
|
|
primary := &stubStream{}
|
|
result := NewTeeStream(primary, nil, "bot1", "telegram")
|
|
if result != primary {
|
|
t.Fatal("expected primary stream returned when observer is nil")
|
|
}
|
|
}
|
|
|
|
func TestTeeStream_Push(t *testing.T) {
|
|
primary := &stubStream{}
|
|
obs := &recordingObserver{}
|
|
stream := NewTeeStream(primary, obs, "bot1", "telegram")
|
|
|
|
event := StreamEvent{Type: StreamEventDelta, Delta: "hello"}
|
|
if err := stream.Push(context.Background(), event); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
primary.mu.Lock()
|
|
if len(primary.events) != 1 {
|
|
t.Fatalf("expected 1 primary event, got %d", len(primary.events))
|
|
}
|
|
if primary.events[0].Delta != "hello" {
|
|
t.Fatalf("unexpected primary delta: %s", primary.events[0].Delta)
|
|
}
|
|
primary.mu.Unlock()
|
|
|
|
recorded := obs.recorded()
|
|
if len(recorded) != 1 {
|
|
t.Fatalf("expected 1 observed event, got %d", len(recorded))
|
|
}
|
|
if recorded[0].BotID != "bot1" {
|
|
t.Fatalf("unexpected botID: %s", recorded[0].BotID)
|
|
}
|
|
if recorded[0].Source != "telegram" {
|
|
t.Fatalf("unexpected source: %s", recorded[0].Source)
|
|
}
|
|
if recorded[0].Event.Delta != "hello" {
|
|
t.Fatalf("unexpected observed delta: %s", recorded[0].Event.Delta)
|
|
}
|
|
}
|
|
|
|
func TestTeeStream_Close(t *testing.T) {
|
|
primary := &stubStream{}
|
|
obs := &recordingObserver{}
|
|
stream := NewTeeStream(primary, obs, "bot1", "telegram")
|
|
|
|
if err := stream.Close(context.Background()); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
primary.mu.Lock()
|
|
if !primary.closed {
|
|
t.Fatal("expected primary stream to be closed")
|
|
}
|
|
primary.mu.Unlock()
|
|
|
|
if len(obs.recorded()) != 0 {
|
|
t.Fatal("close should not produce observer events")
|
|
}
|
|
}
|
|
|
|
func TestTeeStream_MultipleEvents(t *testing.T) {
|
|
primary := &stubStream{}
|
|
obs := &recordingObserver{}
|
|
stream := NewTeeStream(primary, obs, "bot1", "feishu")
|
|
|
|
events := []StreamEvent{
|
|
{Type: StreamEventStatus, Status: StreamStatusStarted},
|
|
{Type: StreamEventDelta, Delta: "chunk1"},
|
|
{Type: StreamEventDelta, Delta: "chunk2"},
|
|
{Type: StreamEventFinal, Final: &StreamFinalizePayload{Message: Message{Text: "done"}}},
|
|
{Type: StreamEventStatus, Status: StreamStatusCompleted},
|
|
}
|
|
for _, event := range events {
|
|
if err := stream.Push(context.Background(), event); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
primary.mu.Lock()
|
|
if len(primary.events) != len(events) {
|
|
t.Fatalf("expected %d primary events, got %d", len(events), len(primary.events))
|
|
}
|
|
primary.mu.Unlock()
|
|
|
|
recorded := obs.recorded()
|
|
if len(recorded) != len(events) {
|
|
t.Fatalf("expected %d observed events, got %d", len(events), len(recorded))
|
|
}
|
|
for i, r := range recorded {
|
|
if r.Source != "feishu" {
|
|
t.Fatalf("event %d: unexpected source %s", i, r.Source)
|
|
}
|
|
}
|
|
}
|