feat(coredeno): Tier 4 marketplace install pipeline — clone, verify, register, auto-load

Wire the marketplace to actually install modules from Git repos, verify
manifest signatures, track installations in the store, and auto-load them
as Workers at startup. A module goes from marketplace entry to running
Worker with Install() + LoadModule().

- Add Store.GetAll() for group-scoped key listing
- Create marketplace.Installer with Install/Remove/Update/Installed
- Export manifest.MarshalYAML for test fixtures
- Wire installer into Service with auto-load on startup (step 8)
- Expose Service.Installer() accessor
- Full integration test: install → load → verify store write → unload → remove

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-02-18 08:04:13 +00:00
parent ad6a466459
commit 9899398153
No known key found for this signature in database
GPG key ID: AF404715446AEB41
8 changed files with 690 additions and 4 deletions

View file

@ -12,6 +12,7 @@ import (
pb "forge.lthn.ai/core/go/pkg/coredeno/proto"
core "forge.lthn.ai/core/go/pkg/framework/core"
"forge.lthn.ai/core/go/pkg/marketplace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
@ -339,3 +340,160 @@ permissions:
assert.NoError(t, err)
assert.False(t, svc.sidecar.IsRunning(), "Deno sidecar should be stopped")
}
// createModuleRepo creates a git repo containing a test module with manifest + main.ts.
// The module's init() writes to the store to prove the I/O bridge works.
func createModuleRepo(t *testing.T, code string) string {
t.Helper()
dir := filepath.Join(t.TempDir(), code+"-repo")
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "view.yml"), []byte(`
code: `+code+`
name: Test Module `+code+`
version: "1.0"
permissions:
read: ["./"]
`), 0644))
// Module that writes to store to prove it ran
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.ts"), []byte(`
export async function init(core: any) {
await core.storeSet("`+code+`", "installed", "yes");
}
`), 0644))
gitCmd := func(args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{
"-C", dir, "-c", "user.email=test@test.com", "-c", "user.name=test",
}, args...)...)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "git %v: %s", args, string(out))
}
gitCmd("init")
gitCmd("add", ".")
gitCmd("commit", "-m", "init")
return dir
}
func TestIntegration_Tier4_MarketplaceInstall_Good(t *testing.T) {
denoPath := findDeno(t)
tmpDir := t.TempDir()
sockPath := filepath.Join(tmpDir, "core.sock")
denoSockPath := filepath.Join(tmpDir, "deno.sock")
// Write app manifest
coreDir := filepath.Join(tmpDir, ".core")
require.NoError(t, os.MkdirAll(coreDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(coreDir, "view.yml"), []byte(`
code: tier4-test
name: Tier 4 Test
version: "1.0"
permissions:
read: ["./"]
`), 0644))
entryPoint := runtimeEntryPoint(t)
opts := Options{
DenoPath: denoPath,
SocketPath: sockPath,
DenoSocketPath: denoSockPath,
AppRoot: tmpDir,
StoreDBPath: ":memory:",
SidecarArgs: []string{"run", "-A", "--unstable-worker-options", entryPoint},
}
c, err := core.New()
require.NoError(t, err)
factory := NewServiceFactory(opts)
result, err := factory(c)
require.NoError(t, err)
svc := result.(*Service)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err = svc.OnStartup(ctx)
require.NoError(t, err)
// Verify sidecar and Deno client are up
require.Eventually(t, func() bool {
_, err := os.Stat(denoSockPath)
return err == nil
}, 10*time.Second, 50*time.Millisecond, "deno socket should appear")
require.NotNil(t, svc.DenoClient(), "DenoClient should be connected")
require.NotNil(t, svc.Installer(), "Installer should be available")
// Create a test module repo and install it
moduleRepo := createModuleRepo(t, "market-mod")
err = svc.Installer().Install(ctx, marketplace.Module{
Code: "market-mod",
Repo: moduleRepo,
})
require.NoError(t, err)
// Verify the module was installed on disk
modulesDir := filepath.Join(tmpDir, "modules", "market-mod")
require.DirExists(t, modulesDir)
// Verify Installed() returns it
installed, err := svc.Installer().Installed()
require.NoError(t, err)
require.Len(t, installed, 1)
assert.Equal(t, "market-mod", installed[0].Code)
assert.Equal(t, "1.0", installed[0].Version)
// Load the installed module into the Deno runtime
mod := installed[0]
loadResp, err := svc.DenoClient().LoadModule(mod.Code, mod.EntryPoint, ModulePermissions{
Read: mod.Permissions.Read,
})
require.NoError(t, err)
assert.True(t, loadResp.Ok)
// Wait for module to reach RUNNING
require.Eventually(t, func() bool {
resp, err := svc.DenoClient().ModuleStatus("market-mod")
return err == nil && resp.Status == "RUNNING"
}, 10*time.Second, 100*time.Millisecond, "installed module should be RUNNING")
// Verify the module wrote to the store via I/O bridge
conn, err := grpc.NewClient(
"unix://"+sockPath,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
defer conn.Close()
coreClient := pb.NewCoreServiceClient(conn)
require.Eventually(t, func() bool {
resp, err := coreClient.StoreGet(ctx, &pb.StoreGetRequest{
Group: "market-mod", Key: "installed",
})
return err == nil && resp.Found && resp.Value == "yes"
}, 5*time.Second, 100*time.Millisecond, "installed module should have written to store via I/O bridge")
// Unload and remove
unloadResp, err := svc.DenoClient().UnloadModule("market-mod")
require.NoError(t, err)
assert.True(t, unloadResp.Ok)
err = svc.Installer().Remove("market-mod")
require.NoError(t, err)
assert.NoDirExists(t, modulesDir, "module directory should be removed")
installed2, err := svc.Installer().Installed()
require.NoError(t, err)
assert.Empty(t, installed2, "no modules should be installed after remove")
// Clean shutdown
err = svc.OnShutdown(context.Background())
assert.NoError(t, err)
assert.False(t, svc.sidecar.IsRunning(), "Deno sidecar should be stopped")
}

View file

@ -4,11 +4,13 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"time"
core "forge.lthn.ai/core/go/pkg/framework/core"
"forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/go/pkg/manifest"
"forge.lthn.ai/core/go/pkg/marketplace"
"forge.lthn.ai/core/go/pkg/store"
)
@ -26,6 +28,7 @@ type Service struct {
grpcCancel context.CancelFunc
grpcDone chan error
denoClient *DenoClient
installer *marketplace.Installer
}
// NewServiceFactory returns a factory function for framework registration via WithService.
@ -116,6 +119,27 @@ func (s *Service) OnStartup(ctx context.Context) error {
}
}
// 8. Create installer and auto-load installed modules
if opts.AppRoot != "" {
modulesDir := filepath.Join(opts.AppRoot, "modules")
s.installer = marketplace.NewInstaller(modulesDir, s.store)
if s.denoClient != nil {
installed, listErr := s.installer.Installed()
if listErr == nil {
for _, mod := range installed {
perms := ModulePermissions{
Read: mod.Permissions.Read,
Write: mod.Permissions.Write,
Net: mod.Permissions.Net,
Run: mod.Permissions.Run,
}
s.denoClient.LoadModule(mod.Code, mod.EntryPoint, perms)
}
}
}
}
return nil
}
@ -159,6 +183,12 @@ func (s *Service) DenoClient() *DenoClient {
return s.denoClient
}
// Installer returns the marketplace module installer.
// Returns nil if AppRoot was not set.
func (s *Service) Installer() *marketplace.Installer {
return s.installer
}
// waitForSocket polls until a Unix socket file appears or the context/timeout expires.
func waitForSocket(ctx context.Context, path string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)

View file

@ -11,8 +11,8 @@ import (
const manifestPath = ".core/view.yml"
// marshalYAML serializes a manifest to YAML bytes.
func marshalYAML(m *Manifest) ([]byte, error) {
// MarshalYAML serializes a manifest to YAML bytes.
func MarshalYAML(m *Manifest) ([]byte, error) {
return yaml.Marshal(m)
}

View file

@ -39,7 +39,7 @@ func TestLoadVerified_Good(t *testing.T) {
}
_ = Sign(m, priv)
raw, _ := marshalYAML(m)
raw, _ := MarshalYAML(m)
fs := io.NewMockMedium()
fs.Files[".core/view.yml"] = string(raw)
@ -53,7 +53,7 @@ func TestLoadVerified_Bad_Tampered(t *testing.T) {
m := &Manifest{Code: "app", Version: "1.0.0"}
_ = Sign(m, priv)
raw, _ := marshalYAML(m)
raw, _ := MarshalYAML(m)
tampered := "code: evil\n" + string(raw)[6:]
fs := io.NewMockMedium()
fs.Files[".core/view.yml"] = tampered

View file

@ -0,0 +1,194 @@
package marketplace
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/go/pkg/manifest"
"forge.lthn.ai/core/go/pkg/store"
)
const storeGroup = "_modules"
// Installer handles module installation from Git repos.
type Installer struct {
modulesDir string
store *store.Store
}
// NewInstaller creates a new module installer.
func NewInstaller(modulesDir string, st *store.Store) *Installer {
return &Installer{
modulesDir: modulesDir,
store: st,
}
}
// InstalledModule holds stored metadata about an installed module.
type InstalledModule struct {
Code string `json:"code"`
Name string `json:"name"`
Version string `json:"version"`
Repo string `json:"repo"`
EntryPoint string `json:"entry_point"`
Permissions manifest.Permissions `json:"permissions"`
InstalledAt string `json:"installed_at"`
}
// Install clones a module repo, verifies its manifest signature, and registers it.
func (i *Installer) Install(ctx context.Context, mod Module) error {
// Check if already installed
if _, err := i.store.Get(storeGroup, mod.Code); err == nil {
return fmt.Errorf("marketplace: module %q already installed", mod.Code)
}
dest := filepath.Join(i.modulesDir, mod.Code)
if err := os.MkdirAll(i.modulesDir, 0755); err != nil {
return fmt.Errorf("marketplace: mkdir: %w", err)
}
if err := gitClone(ctx, mod.Repo, dest); err != nil {
return fmt.Errorf("marketplace: clone %s: %w", mod.Repo, err)
}
// On any error after clone, clean up the directory
cleanup := true
defer func() {
if cleanup {
os.RemoveAll(dest)
}
}()
medium, err := io.NewSandboxed(dest)
if err != nil {
return fmt.Errorf("marketplace: medium: %w", err)
}
m, err := loadManifest(medium, mod.SignKey)
if err != nil {
return err
}
entryPoint := filepath.Join(dest, "main.ts")
installed := InstalledModule{
Code: mod.Code,
Name: m.Name,
Version: m.Version,
Repo: mod.Repo,
EntryPoint: entryPoint,
Permissions: m.Permissions,
InstalledAt: time.Now().UTC().Format(time.RFC3339),
}
data, err := json.Marshal(installed)
if err != nil {
return fmt.Errorf("marketplace: marshal: %w", err)
}
if err := i.store.Set(storeGroup, mod.Code, string(data)); err != nil {
return fmt.Errorf("marketplace: store: %w", err)
}
cleanup = false
return nil
}
// Remove uninstalls a module by deleting its files and store entry.
func (i *Installer) Remove(code string) error {
if _, err := i.store.Get(storeGroup, code); err != nil {
return fmt.Errorf("marketplace: module %q not installed", code)
}
dest := filepath.Join(i.modulesDir, code)
os.RemoveAll(dest)
return i.store.Delete(storeGroup, code)
}
// Update pulls latest changes and re-verifies the manifest.
func (i *Installer) Update(ctx context.Context, code string) error {
raw, err := i.store.Get(storeGroup, code)
if err != nil {
return fmt.Errorf("marketplace: module %q not installed", code)
}
var installed InstalledModule
if err := json.Unmarshal([]byte(raw), &installed); err != nil {
return fmt.Errorf("marketplace: unmarshal: %w", err)
}
dest := filepath.Join(i.modulesDir, code)
cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("marketplace: pull: %s: %w", strings.TrimSpace(string(output)), err)
}
// Reload manifest
medium, mErr := io.NewSandboxed(dest)
if mErr != nil {
return fmt.Errorf("marketplace: medium: %w", mErr)
}
m, mErr := manifest.Load(medium, ".")
if mErr != nil {
return fmt.Errorf("marketplace: reload manifest: %w", mErr)
}
// Update stored metadata
installed.Name = m.Name
installed.Version = m.Version
installed.Permissions = m.Permissions
data, err := json.Marshal(installed)
if err != nil {
return fmt.Errorf("marketplace: marshal: %w", err)
}
return i.store.Set(storeGroup, code, string(data))
}
// Installed returns all installed module metadata.
func (i *Installer) Installed() ([]InstalledModule, error) {
all, err := i.store.GetAll(storeGroup)
if err != nil {
return nil, fmt.Errorf("marketplace: list: %w", err)
}
var modules []InstalledModule
for _, raw := range all {
var m InstalledModule
if err := json.Unmarshal([]byte(raw), &m); err != nil {
continue
}
modules = append(modules, m)
}
return modules, nil
}
// loadManifest loads and optionally verifies a module manifest.
func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error) {
if signKey != "" {
pubBytes, err := hex.DecodeString(signKey)
if err != nil {
return nil, fmt.Errorf("marketplace: decode sign key: %w", err)
}
return manifest.LoadVerified(medium, ".", pubBytes)
}
return manifest.Load(medium, ".")
}
// gitClone clones a repository with --depth=1.
func gitClone(ctx context.Context, repo, dest string) error {
cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", repo, dest)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err)
}
return nil
}

View file

@ -0,0 +1,263 @@
package marketplace
import (
"context"
"crypto/ed25519"
"encoding/hex"
"os"
"os/exec"
"path/filepath"
"testing"
"forge.lthn.ai/core/go/pkg/manifest"
"forge.lthn.ai/core/go/pkg/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// createTestRepo creates a bare-bones git repo with a manifest and main.ts.
// Returns the repo path (usable as Module.Repo for local clone).
func createTestRepo(t *testing.T, code, version string) string {
t.Helper()
dir := filepath.Join(t.TempDir(), code)
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0755))
manifestYAML := "code: " + code + "\nname: Test " + code + "\nversion: \"" + version + "\"\n"
require.NoError(t, os.WriteFile(
filepath.Join(dir, ".core", "view.yml"),
[]byte(manifestYAML), 0644,
))
require.NoError(t, os.WriteFile(
filepath.Join(dir, "main.ts"),
[]byte("export async function init(core: any) {}\n"), 0644,
))
runGit(t, dir, "init")
runGit(t, dir, "add", ".")
runGit(t, dir, "commit", "-m", "init")
return dir
}
// createSignedTestRepo creates a git repo with a signed manifest.
// Returns (repo path, hex-encoded public key).
func createSignedTestRepo(t *testing.T, code, version string) (string, string) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
dir := filepath.Join(t.TempDir(), code)
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0755))
m := &manifest.Manifest{
Code: code,
Name: "Test " + code,
Version: version,
}
require.NoError(t, manifest.Sign(m, priv))
data, err := manifest.MarshalYAML(m)
require.NoError(t, err)
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "view.yml"), data, 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.ts"), []byte("export async function init(core: any) {}\n"), 0644))
runGit(t, dir, "init")
runGit(t, dir, "add", ".")
runGit(t, dir, "commit", "-m", "init")
return dir, hex.EncodeToString(pub)
}
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.email=test@test.com", "-c", "user.name=test"}, args...)...)
out, err := cmd.CombinedOutput()
require.NoError(t, err, "git %v: %s", args, string(out))
}
func TestInstall_Good(t *testing.T) {
repo := createTestRepo(t, "hello-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
err = inst.Install(context.Background(), Module{
Code: "hello-mod",
Repo: repo,
})
require.NoError(t, err)
// Verify directory exists
_, err = os.Stat(filepath.Join(modulesDir, "hello-mod", "main.ts"))
assert.NoError(t, err, "main.ts should exist in installed module")
// Verify store entry
raw, err := st.Get("_modules", "hello-mod")
require.NoError(t, err)
assert.Contains(t, raw, `"code":"hello-mod"`)
assert.Contains(t, raw, `"version":"1.0"`)
}
func TestInstall_Good_Signed(t *testing.T) {
repo, signKey := createSignedTestRepo(t, "signed-mod", "2.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
err = inst.Install(context.Background(), Module{
Code: "signed-mod",
Repo: repo,
SignKey: signKey,
})
require.NoError(t, err)
raw, err := st.Get("_modules", "signed-mod")
require.NoError(t, err)
assert.Contains(t, raw, `"version":"2.0"`)
}
func TestInstall_Bad_AlreadyInstalled(t *testing.T) {
repo := createTestRepo(t, "dup-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
mod := Module{Code: "dup-mod", Repo: repo}
require.NoError(t, inst.Install(context.Background(), mod))
err = inst.Install(context.Background(), mod)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already installed")
}
func TestInstall_Bad_InvalidSignature(t *testing.T) {
// Sign with key A, verify with key B
repo, _ := createSignedTestRepo(t, "bad-sig", "1.0")
_, wrongKey := createSignedTestRepo(t, "dummy", "1.0") // different key
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
err = inst.Install(context.Background(), Module{
Code: "bad-sig",
Repo: repo,
SignKey: wrongKey,
})
assert.Error(t, err)
// Verify directory was cleaned up
_, statErr := os.Stat(filepath.Join(modulesDir, "bad-sig"))
assert.True(t, os.IsNotExist(statErr), "directory should be cleaned up on failure")
}
func TestRemove_Good(t *testing.T) {
repo := createTestRepo(t, "rm-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
require.NoError(t, inst.Install(context.Background(), Module{Code: "rm-mod", Repo: repo}))
err = inst.Remove("rm-mod")
require.NoError(t, err)
// Directory gone
_, statErr := os.Stat(filepath.Join(modulesDir, "rm-mod"))
assert.True(t, os.IsNotExist(statErr))
// Store entry gone
_, err = st.Get("_modules", "rm-mod")
assert.Error(t, err)
}
func TestRemove_Bad_NotInstalled(t *testing.T) {
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(t.TempDir(), st)
err = inst.Remove("nonexistent")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not installed")
}
func TestInstalled_Good(t *testing.T) {
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
repo1 := createTestRepo(t, "mod-a", "1.0")
repo2 := createTestRepo(t, "mod-b", "2.0")
require.NoError(t, inst.Install(context.Background(), Module{Code: "mod-a", Repo: repo1}))
require.NoError(t, inst.Install(context.Background(), Module{Code: "mod-b", Repo: repo2}))
installed, err := inst.Installed()
require.NoError(t, err)
assert.Len(t, installed, 2)
codes := map[string]bool{}
for _, m := range installed {
codes[m.Code] = true
}
assert.True(t, codes["mod-a"])
assert.True(t, codes["mod-b"])
}
func TestInstalled_Good_Empty(t *testing.T) {
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(t.TempDir(), st)
installed, err := inst.Installed()
require.NoError(t, err)
assert.Empty(t, installed)
}
func TestUpdate_Good(t *testing.T) {
repo := createTestRepo(t, "upd-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
inst := NewInstaller(modulesDir, st)
require.NoError(t, inst.Install(context.Background(), Module{Code: "upd-mod", Repo: repo}))
// Update the origin repo
newManifest := "code: upd-mod\nname: Updated Module\nversion: \"2.0\"\n"
require.NoError(t, os.WriteFile(filepath.Join(repo, ".core", "view.yml"), []byte(newManifest), 0644))
runGit(t, repo, "add", ".")
runGit(t, repo, "commit", "-m", "bump version")
err = inst.Update(context.Background(), "upd-mod")
require.NoError(t, err)
// Verify updated metadata
installed, err := inst.Installed()
require.NoError(t, err)
require.Len(t, installed, 1)
assert.Equal(t, "2.0", installed[0].Version)
assert.Equal(t, "Updated Module", installed[0].Name)
}

View file

@ -95,6 +95,25 @@ func (s *Store) DeleteGroup(group string) error {
return nil
}
// GetAll returns all key-value pairs in a group.
func (s *Store) GetAll(group string) (map[string]string, error) {
rows, err := s.db.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {
return nil, fmt.Errorf("store.GetAll: %w", err)
}
defer rows.Close()
result := make(map[string]string)
for rows.Next() {
var k, v string
if err := rows.Scan(&k, &v); err != nil {
return nil, fmt.Errorf("store.GetAll: scan: %w", err)
}
result[k] = v
}
return result, nil
}
// Render loads all key-value pairs from a group and renders a Go template.
func (s *Store) Render(tmplStr, group string) (string, error) {
rows, err := s.db.Query("SELECT key, value FROM kv WHERE grp = ?", group)

View file

@ -66,6 +66,28 @@ func TestDeleteGroup_Good(t *testing.T) {
assert.Equal(t, 0, n)
}
func TestGetAll_Good(t *testing.T) {
s, _ := New(":memory:")
defer s.Close()
_ = s.Set("grp", "a", "1")
_ = s.Set("grp", "b", "2")
_ = s.Set("other", "c", "3")
all, err := s.GetAll("grp")
require.NoError(t, err)
assert.Equal(t, map[string]string{"a": "1", "b": "2"}, all)
}
func TestGetAll_Good_Empty(t *testing.T) {
s, _ := New(":memory:")
defer s.Close()
all, err := s.GetAll("empty")
require.NoError(t, err)
assert.Empty(t, all)
}
func TestRender_Good(t *testing.T) {
s, _ := New(":memory:")
defer s.Close()