Files
Memoh/internal/memory/adapters/builtin/builtin_test.go
T
Acbox Liu bca13a13fa feat(web): introduce a brand new web ui (#281)
* feat(web): introduce a brand new web ui

* refactor(ui): align chat sidebar and UI components with Figma design

- Restyle chat page sidebar: header with icon/title, search input,
  section labels, and "new session" footer button
- Simplify bot-sidebar and session-sidebar to card-based layout
  matching Figma session card design (58px height, 26px avatar, status dots)
- Update master-detail-sidebar-layout with bg-sidebar and 53px header
- Unify border-radius across UI components to rounded-lg (8px):
  Card, Toggle, Alert, Popover, Item; Dialog uses rounded-xl (12px)

* refactor(ui): move shared theme and design tokens from web to ui package

CSS variables, @theme inline mappings, @custom-variant, and base layer
styles now live in @memohai/ui/style.css. The web app imports them via
@import "@memohai/ui/style.css", keeping only the Tailwind entry point
and web-specific imports (markstream-vue, @source).

* refactor(ui): apply flat design system from Figma spec

Overhaul @memohai/ui component styles to match the new "high-contrast,
flat" design language defined in the Figma design spec (DESIGN.md).

Theme:
- --primary-foreground: pure white -> #fafafa
- --ring: purple -> foreground color (focus rings no longer use brand purple)

Atoms (zero shadow, monochrome):
- Button: default bg-primary -> bg-foreground; add explicit "primary" variant for Send CTA
- Badge: rounded-full -> rounded-sm; default bg-primary -> bg-foreground; add warning/outline/size variants
- Alert: rounded-lg -> rounded-[10px]; remove shadow-sm; destructive drops bg-red-50
- Card: add shadow-lg, rounded-lg -> rounded-xl, py-6 -> p-6
- Input/Textarea: remove shadow, text-sm -> text-[16px], focus ring non-purple
- Checkbox: checked bg-primary -> bg-foreground
- Switch: checked bg-primary -> bg-foreground
- RadioGroup: indicator fill-primary -> fill-foreground
- Slider: range/thumb border-primary -> border-foreground

Floating panels (shadow-md):
- DropdownMenu/Combobox/Select/ContextMenu Content: shadow-lg -> shadow-md
- Sheet: shadow-2xl -> shadow-lg
- MenuItem destructive focus: bg-red-50 -> bg-accent

Other:
- Pagination active: bg-foreground text-background (black, not purple)
- Item variants: bg-transparent -> bg-background/bg-accent
- Tabs active: shadow-sm -> border-border
- Toggle: remove shadow-xs, unify hover to accent
- SelectTrigger/NativeSelect: remove shadow, unify focus ring

Docs:
- Add packages/ui/DESIGN.md with full design system spec
- Simplify apps/web/AGENTS.md, remove duplicated design info, reference DESIGN.md

* refactor(chat-ui): restructure chat page components and styles (#288)

* refactor(chat-ui): restructure chat page components and styles

* feat(chat): add collapsible sidebar for both sides

* feat(ui): add PinInput and BadgeCount components, align styles with Figma spec

New components:
- PinInput (OTP input): PinInput, PinInputGroup, PinInputSlot, PinInputSeparator
  based on reka-ui PinInput primitives with flat border-stitching design
- BadgeCount: circular numeric counter with default/destructive/secondary variants

Style updates to match Figma design:
- Sonner: border-radius from 1rem to var(--radius-lg) (10px)
- Table: add border border-border rounded-sm to container
- TagsInput: remove shadow-xs, rounded-md -> rounded-lg, ring-[3px] -> ring-2

Updated DESIGN.md with all new component specifications.

* chore: move up css to ui package

* refactor: change npm package from @memoh to @memohai

* Feat/chat layout (#295)

* refactor(chat-ui): restructure chat page components and styles

* feat(chat): add collapsible sidebar for both sides

* fix: update chat page icon

* style: refine UI components appearance

* style: refine UI components appearance

* chore(ci): update lock file

* refactor: new layout

* chore: adjust style

* fix: tauri ui size

* chore: remove bot session metadata

* refactor: text size and muted color

* fix: indirect height of bot-details pages

* feat: add 5 icons

* refactor: polish chat flow and settings navigation labels

Persist chat selection across pages, simplify provider/settings sidebars, and refine chat/session UX so navigation and composer behavior feel consistent without extra session/provider jumps.

* docs(web): refresh AGENTS frontend architecture guide

Expand and align the web AGENTS documentation with the current route structure, component inventory, chat transport flow, and store responsibilities so implementation guidance matches the codebase.

---------

Co-authored-by: Quincy <69751197+dqygit@users.noreply.github.com>
2026-03-28 19:15:39 +08:00

231 lines
6.8 KiB
Go

package builtin
import (
"context"
"log/slog"
"strings"
"testing"
"github.com/memohai/memoh/internal/config"
adapters "github.com/memohai/memoh/internal/memory/adapters"
"github.com/memohai/memoh/internal/memory/sparse"
)
func TestBuiltinProviderNilService(t *testing.T) {
t.Parallel()
p := NewBuiltinProvider(slog.Default(), nil, nil, nil)
if p.Type() != BuiltinType {
t.Fatalf("expected type %q, got %q", BuiltinType, p.Type())
}
result, err := p.OnBeforeChat(context.Background(), adapters.BeforeChatRequest{
BotID: "bot-1",
Query: "hello",
})
if err != nil {
t.Fatalf("OnBeforeChat error: %v", err)
}
if result != nil {
t.Fatalf("expected nil result for nil service, got %+v", result)
}
}
func TestBuiltinProviderOnBeforeChatEmptyQuery(t *testing.T) {
t.Parallel()
encoder := &fakeSparseEncoder{}
index := newFakeSparseIndex(encoder)
store := newFakeSparseStore()
runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}
p := NewBuiltinProvider(slog.Default(), runtime, nil, nil)
result, err := p.OnBeforeChat(context.Background(), adapters.BeforeChatRequest{
BotID: "bot-1",
Query: "",
})
if err != nil {
t.Fatalf("OnBeforeChat error: %v", err)
}
if result != nil {
t.Fatal("expected nil result for empty query")
}
}
func TestBuiltinProviderContextPackingProducesMemoryContextTags(t *testing.T) {
t.Parallel()
encoder := &fakeSparseEncoder{}
index := newFakeSparseIndex(encoder)
store := newFakeSparseStore()
runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}
p := NewBuiltinProvider(slog.Default(), runtime, nil, nil)
_ = p.OnAfterChat(context.Background(), adapters.AfterChatRequest{
BotID: "bot-1",
Messages: []adapters.Message{{Role: "user", Content: "I like green tea"}},
})
_ = p.OnAfterChat(context.Background(), adapters.AfterChatRequest{
BotID: "bot-1",
Messages: []adapters.Message{{Role: "user", Content: "I work in Tokyo"}},
})
result, err := p.OnBeforeChat(context.Background(), adapters.BeforeChatRequest{
BotID: "bot-1",
Query: "tea",
})
if err != nil {
t.Fatalf("OnBeforeChat error: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
return
}
if !strings.Contains(result.ContextText, "<memory-context>") {
t.Fatalf("expected memory-context tags, got: %s", result.ContextText)
}
if !strings.Contains(result.ContextText, "</memory-context>") {
t.Fatalf("expected closing memory-context tag, got: %s", result.ContextText)
}
}
func TestBuiltinProviderApplyProviderConfig(t *testing.T) {
t.Parallel()
p := NewBuiltinProvider(slog.Default(), nil, nil, nil)
p.ApplyProviderConfig(map[string]any{
"context_target_items": float64(10),
"context_max_total_chars": float64(3000),
})
if p.packer.TargetItems != 10 {
t.Fatalf("expected TargetItems=10, got %d", p.packer.TargetItems)
}
if p.packer.MaxTotalChars != 3000 {
t.Fatalf("expected MaxTotalChars=3000, got %d", p.packer.MaxTotalChars)
}
if p.packer.MinItemChars != defaultPackerConfig.MinItemChars {
t.Fatalf("expected MinItemChars to remain default, got %d", p.packer.MinItemChars)
}
}
func TestBuiltinProviderApplyProviderConfigNil(t *testing.T) {
t.Parallel()
p := NewBuiltinProvider(slog.Default(), nil, nil, nil)
p.ApplyProviderConfig(nil)
if p.packer.TargetItems != defaultPackerConfig.TargetItems {
t.Fatalf("expected default TargetItems, got %d", p.packer.TargetItems)
}
}
func TestIntFromConfig(t *testing.T) {
t.Parallel()
cases := []struct {
name string
m map[string]any
key string
expected int
}{
{"float64", map[string]any{"k": float64(42)}, "k", 42},
{"int", map[string]any{"k": 10}, "k", 10},
{"missing", map[string]any{}, "k", 0},
{"nil_map", nil, "k", 0},
{"string_value", map[string]any{"k": "abc"}, "k", 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := intFromConfig(tc.m, tc.key)
if got != tc.expected {
t.Fatalf("expected %d, got %d", tc.expected, got)
}
})
}
}
func TestBuiltinProviderBadServiceTypeDoesNotPanic(t *testing.T) {
t.Parallel()
p := NewBuiltinProvider(slog.Default(), "not a runtime", nil, nil)
if p.service != nil {
t.Fatal("expected nil service for non-memoryRuntime value")
}
_, err := p.Search(context.Background(), adapters.SearchRequest{BotID: "b", Query: "q"})
if err == nil {
t.Fatal("expected error from nil service")
}
}
func TestBuiltinProviderCRUDErrorsWithNilService(t *testing.T) {
t.Parallel()
p := NewBuiltinProvider(slog.Default(), nil, nil, nil)
if _, err := p.Add(context.Background(), adapters.AddRequest{}); err == nil {
t.Fatal("expected Add error")
}
if _, err := p.GetAll(context.Background(), adapters.GetAllRequest{}); err == nil {
t.Fatal("expected GetAll error")
}
if _, err := p.Update(context.Background(), adapters.UpdateRequest{}); err == nil {
t.Fatal("expected Update error")
}
if _, err := p.Delete(context.Background(), "x"); err == nil {
t.Fatal("expected Delete error")
}
if _, err := p.DeleteBatch(context.Background(), []string{"x"}); err == nil {
t.Fatal("expected DeleteBatch error")
}
if _, err := p.DeleteAll(context.Background(), adapters.DeleteAllRequest{}); err == nil {
t.Fatal("expected DeleteAll error")
}
if _, err := p.Compact(context.Background(), nil, 0.5, 0); err == nil {
t.Fatal("expected Compact error")
}
if _, err := p.Usage(context.Background(), nil); err == nil {
t.Fatal("expected Usage error")
}
if _, err := p.Status(context.Background(), "b"); err == nil {
t.Fatal("expected Status error")
}
if _, err := p.Rebuild(context.Background(), "b"); err == nil {
t.Fatal("expected Rebuild error")
}
}
func TestNewBuiltinRuntimeFromConfig_DefaultReturnsFileRuntime(t *testing.T) {
t.Parallel()
sentinel := "file-runtime-sentinel"
rt, err := NewBuiltinRuntimeFromConfig(nil, nil, sentinel, nil, nil, defaultTestConfig())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rt != sentinel {
t.Fatalf("expected file runtime sentinel, got %v", rt)
}
}
func TestNewBuiltinRuntimeFromConfig_DenseErrorPropagates(t *testing.T) {
t.Parallel()
cfg := map[string]any{"memory_mode": "dense"}
_, err := NewBuiltinRuntimeFromConfig(nil, cfg, "fallback", nil, nil, defaultTestConfig())
if err == nil {
t.Fatal("expected error for dense mode without embedding_model_id")
}
}
func TestNewBuiltinRuntimeFromConfig_SparseErrorPropagates(t *testing.T) {
t.Parallel()
cfg := map[string]any{"memory_mode": "sparse"}
_, err := NewBuiltinRuntimeFromConfig(nil, cfg, "fallback", nil, nil, defaultTestConfig())
if err == nil {
t.Fatal("expected error for sparse mode without encoder base URL")
}
}
func defaultTestConfig() config.Config {
return config.Config{}
}
// Fakes from sparse_runtime_test.go are in the same package and accessible.
var _ sparseEncoder = (*fakeSparseEncoder)(nil)
func init() {
_ = sparse.SparseVector{}
}