Builds on merged PR-1..7 (PR-8 in queue). Pure docs; no code.
What ships:
* docs/memory-plugins/README.md — contract overview, capability
negotiation, deployment models, replacement workflow
* docs/memory-plugins/testing-your-plugin.md — using the contract
test harness to validate wire compatibility, what the harness
DOES NOT cover (capability accuracy, TTL eviction, concurrency)
* docs/memory-plugins/pinecone-example/README.md — worked example
of a Pinecone-backed plugin: capability mapping (only embedding,
no FTS), wire mapping (memory → vector + metadata), production-
hardening checklist
Documentation strategy:
* Lead with what workspace-server takes care of (security perimeter,
redaction, ACL, GLOBAL audit, prompt-injection wrap) so plugin
authors don't reimplement those layers
* Show three deployment models (same machine / separate container /
self-managed) so operators see their topology
* Capability table makes it explicit what each capability gates so
a plugin that supports only one (e.g. semantic search) is still
a useful plugin
* Pinecone example is honest: shows the skeleton, the wire mapping,
and explicitly calls out what's MISSING from the sketch (batch
commits, TTL janitor, circuit breaker, metrics)
113 lines
3.5 KiB
Markdown
113 lines
3.5 KiB
Markdown
# Testing Your Memory Plugin
|
|
|
|
Once you have a plugin implementing the v1 contract, you can validate
|
|
it against the spec without booting workspace-server.
|
|
|
|
## The contract test harness
|
|
|
|
Workspace-server ships typed Go bindings + round-trip tests in
|
|
`workspace-server/internal/memory/contract/`. The simplest way to
|
|
gain confidence in your plugin's wire compatibility is to point those
|
|
tests at it.
|
|
|
|
A minimal contract suite:
|
|
|
|
```go
|
|
package myplugin_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
mclient "github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/client"
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/memory/contract"
|
|
)
|
|
|
|
func TestMyPlugin_FullRoundTrip(t *testing.T) {
|
|
// Start your plugin somehow (subprocess, in-process, etc.)
|
|
pluginURL := startMyPlugin(t)
|
|
cl := mclient.New(mclient.Config{BaseURL: pluginURL})
|
|
|
|
// 1. Health
|
|
hr, err := cl.Boot(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Boot: %v", err)
|
|
}
|
|
if hr.Status != "ok" {
|
|
t.Errorf("status = %q", hr.Status)
|
|
}
|
|
|
|
// 2. Namespace upsert
|
|
if _, err := cl.UpsertNamespace(context.Background(), "workspace:test-1",
|
|
contract.NamespaceUpsert{Kind: contract.NamespaceKindWorkspace}); err != nil {
|
|
t.Fatalf("UpsertNamespace: %v", err)
|
|
}
|
|
|
|
// 3. Commit memory
|
|
resp, err := cl.CommitMemory(context.Background(), "workspace:test-1",
|
|
contract.MemoryWrite{
|
|
Content: "hello",
|
|
Kind: contract.MemoryKindFact,
|
|
Source: contract.MemorySourceAgent,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CommitMemory: %v", err)
|
|
}
|
|
if resp.ID == "" {
|
|
t.Errorf("plugin must return a non-empty memory id")
|
|
}
|
|
|
|
// 4. Search
|
|
sresp, err := cl.Search(context.Background(), contract.SearchRequest{
|
|
Namespaces: []string{"workspace:test-1"},
|
|
Query: "hello",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(sresp.Memories) == 0 {
|
|
t.Errorf("plugin returned no memories for the query we just wrote")
|
|
}
|
|
|
|
// 5. Forget
|
|
if err := cl.ForgetMemory(context.Background(), resp.ID,
|
|
contract.ForgetRequest{RequestedByNamespace: "workspace:test-1"}); err != nil {
|
|
t.Errorf("ForgetMemory: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## What the harness does NOT cover
|
|
|
|
- **Capability accuracy**: if you list `embedding` you must actually
|
|
do semantic search. The harness can't tell you whether ranking is
|
|
meaningful — only that you don't crash.
|
|
- **TTL eviction**: write a memory with `expires_at` 1 second in the
|
|
future, sleep 2 seconds, search — assert the memory is gone.
|
|
- **Concurrency**: hit your plugin with 100 parallel writes; assert
|
|
no IDs collide.
|
|
- **Recovery**: kill your plugin's storage backend, send a request,
|
|
assert your plugin returns 503 (not 200 with stale data).
|
|
|
|
## Smoke test against workspace-server
|
|
|
|
Once unit-level wire tests pass, run a real workspace-server with your
|
|
plugin URL:
|
|
|
|
```bash
|
|
DATABASE_URL=postgres://... \
|
|
MEMORY_PLUGIN_URL=http://localhost:9100 \
|
|
./workspace-server
|
|
```
|
|
|
|
Then ask an agent to call `commit_memory_v2` and `search_memory`. If
|
|
both round-trip cleanly, you're done.
|
|
|
|
For the full E2E flow (including the namespace resolver, MCP layer,
|
|
and security perimeter), see [PR-11's plugin-swap test](../../workspace-server/test/e2e/memory_plugin_swap_test.go).
|
|
|
|
## Reporting bugs
|
|
|
|
If you find a contract ambiguity or missing edge case, file an issue
|
|
against `Molecule-AI/molecule-core` referencing RFC #2728.
|