mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
9ceabf68c4
Replace the host bind-mount + containerd exec approach with a per-bot
in-container gRPC server (ContainerService, port 9090). All file I/O,
exec, and MCP stdio sessions now go through gRPC instead of running
shell commands or reading host-mounted directories.
Architecture changes:
- cmd/mcp: rewritten as a gRPC server (ContainerService) with full
file and exec API (ReadFile, WriteFile, ListDir, ReadRaw, WriteRaw,
Exec, Stat, Mkdir, Rename, DeleteFile)
- internal/mcp/mcpcontainer: protobuf definitions and generated stubs
- internal/mcp/mcpclient: gRPC client wrapper with connection pool
(Pool) and Provider interface for dependency injection
- mcp.Manager: add per-bot IP cache, gRPC connection pool, and
SetContainerIP/MCPClient methods; remove DataDir/Exec helpers
- containerd.Service: remove ExecTask/ExecTaskStreaming; network setup
now returns NetworkResult{IP} for pool routing
- internal/fs/service.go: deleted (replaced by mcpclient)
- handlers/fs.go: deleted; MCP stdio session logic moved to mcp_stdio.go
- container provider Executor: all tools (read/write/list/edit/exec)
now call gRPC client instead of running shell via exec
- storefs, containerfs, media, skills, memory: all I/O ported to
mcpclient.Provider
Database:
- migration 0022: drop host_path column from containers table
One-time data migration:
- migrateBindMountData: on first Start() after upgrade, copies old
bind-mount data into the container via gRPC, then renames src dir
to prevent re-migration; runs in background goroutine
Bug fixes:
- mcp_stdio: callRaw now returns full JSON-RPC envelope
{"jsonrpc","id","result"|"error"} matching protocol spec;
explicit "initialize" call now advances session init state to
prevent duplicate handshake on next non-initialize call
- mcpclient Pool: properly evict stale gRPC connection after snapshot
replace (container process recreated); use SetContainerIP instead
of direct map write so IP changes always evict pool entry
- migrateBindMountData: walkErr on directories now counted as failure
so partially-walked trees don't get incorrectly marked as migrated
- cmd/mcp/Dockerfile: removed dead file (docker/Dockerfile.mcp is the
canonical production build)
Tests:
- provider_test.go: restored with bufconn in-process gRPC mock
(fakeContainerService + staticProvider), 14 cases covering all 5
tools plus edge cases
- mcp_session_test.go: new, covers JSON-RPC envelope, init state
machine, pending cleanup on cancel/close, readLoop cancel
- storefs/service_test.go: restored (pure function roundtrip tests)
65 lines
1.4 KiB
Go
65 lines
1.4 KiB
Go
package storefs
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFormatAndParseMemoryDayMD_Roundtrip(t *testing.T) {
|
|
items := []MemoryItem{
|
|
{
|
|
ID: "mem_2",
|
|
Memory: "second record",
|
|
Hash: "h2",
|
|
CreatedAt: "2026-03-01T11:15:00Z",
|
|
},
|
|
{
|
|
ID: "mem_1",
|
|
Memory: "first record",
|
|
Hash: "h1",
|
|
CreatedAt: "2026-03-01T09:40:00Z",
|
|
},
|
|
}
|
|
|
|
md := formatMemoryDayMD("2026-03-01", items)
|
|
if !strings.Contains(md, "# Memory 2026-03-01") {
|
|
t.Fatalf("expected header in markdown: %s", md)
|
|
}
|
|
|
|
parsed, err := parseMemoryDayMD(md)
|
|
if err != nil {
|
|
t.Fatalf("parseMemoryDayMD error: %v", err)
|
|
}
|
|
if len(parsed) != 2 {
|
|
t.Fatalf("expected 2 parsed items, got %d", len(parsed))
|
|
}
|
|
// formatMemoryDayMD sorts by created_at ascending.
|
|
if parsed[0].ID != "mem_1" || parsed[1].ID != "mem_2" {
|
|
t.Fatalf("unexpected order after roundtrip: %#v", parsed)
|
|
}
|
|
}
|
|
|
|
func TestParseLegacyMemoryMD(t *testing.T) {
|
|
legacy := `---
|
|
id: mem_legacy
|
|
hash: legacyhash
|
|
created_at: 2026-03-01T09:00:00Z
|
|
updated_at: 2026-03-01T10:00:00Z
|
|
---
|
|
legacy content`
|
|
|
|
item, err := parseLegacyMemoryMD(legacy)
|
|
if err != nil {
|
|
t.Fatalf("parseLegacyMemoryMD error: %v", err)
|
|
}
|
|
if item.ID != "mem_legacy" {
|
|
t.Fatalf("unexpected id: %#v", item)
|
|
}
|
|
if item.Hash != "legacyhash" {
|
|
t.Fatalf("unexpected hash: %#v", item)
|
|
}
|
|
if item.Memory != "legacy content" {
|
|
t.Fatalf("unexpected memory body: %#v", item)
|
|
}
|
|
}
|