* feat: cloud Mount() per HIP-0106
Adds pkg/vfs.Mount(*zip.App, cloud.Deps) error so the unified Hanzo
Cloud binary can blank-import and register VFS alongside every
other Hanzo subsystem.
VFS has no public HTTP surface today — it's an S3-backed virtual
block filesystem driven through FUSE/CLI/sidecar. Mount() exposes
liveness/readiness probes plus a minimal block-CRUD HTTP API so
co-resident subsystems can talk to VFS without falling back to the
CLI:
- GET /v1/vfs/health — always 200
- GET /v1/vfs/readyz — 200 only when an instance is
attached via vfs.SetInstance
- PUT /v1/vfs/blocks — payload upload
- GET /v1/vfs/blocks/:id — payload download
- DELETE /v1/vfs/blocks/:id — backend delete
- GET /v1/vfs/stats — cache/backend counters
cmd/vfsd is a HIP-0106 thin shim that wires backend + age keys from
env (VFSD_BACKEND, VFSD_AGE_KEY) and listens on cloud.Config.ListenAddr.
The existing cmd/vfs CLI (put/get/stats/mount) is untouched.
init() registers with cloud.Register("vfs", 20, …). schema/vfs.zap
adds the minimal Health + Block ZAP interface; full multi-block
File surface lands in follow-up PRs.
Tests:
- pkg/vfs: TestMount_HealthReadyz + TestMount_PutGetRoundtrip green
- existing TestRoundTrip + TestStats unchanged
* refactor: collapse pkg/vfs/ → root for clean import path
Subsystem code lives in package vfs at repo root. Importers now use
`github.com/hanzoai/vfs` (no /pkg/vfs/ nesting). Standalone shim moved
to cmd/vfs/ where applicable.
Per Hanzo Go-stdlib pattern: repo IS the package.
* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
61 lines
1.5 KiB
Go
61 lines
1.5 KiB
Go
package vfs
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestCacheBasic(t *testing.T) {
|
||
c := NewCache(0) // unbounded
|
||
id1 := BlockID(strings.Repeat("a", 64))
|
||
c.Put(id1, []byte("hello"))
|
||
v, ok := c.Get(id1)
|
||
if !ok || string(v) != "hello" {
|
||
t.Fatalf("get hit: ok=%v val=%q", ok, v)
|
||
}
|
||
c.Delete(id1)
|
||
if _, ok := c.Get(id1); ok {
|
||
t.Fatal("delete should remove")
|
||
}
|
||
}
|
||
|
||
func TestCacheLRUEviction(t *testing.T) {
|
||
// Cap at 100 bytes; 4 entries × 50 bytes each → first two evict.
|
||
c := NewCache(100)
|
||
for i := 0; i < 4; i++ {
|
||
id := BlockID(strings.Repeat(string(rune('a'+i)), 64))
|
||
c.Put(id, make([]byte, 50))
|
||
}
|
||
entries, bytesIn, _ := c.Stats()
|
||
if entries > 2 {
|
||
t.Fatalf("expected ≤2 entries after eviction, got %d", entries)
|
||
}
|
||
if bytesIn > 100 {
|
||
t.Fatalf("expected ≤100 bytes after eviction, got %d", bytesIn)
|
||
}
|
||
// Oldest should be gone
|
||
if _, ok := c.Get(BlockID(strings.Repeat("a", 64))); ok {
|
||
t.Fatal("oldest entry should have been evicted")
|
||
}
|
||
}
|
||
|
||
func TestCacheTouchPromotes(t *testing.T) {
|
||
c := NewCache(100)
|
||
idA := BlockID(strings.Repeat("a", 64))
|
||
idB := BlockID(strings.Repeat("b", 64))
|
||
idC := BlockID(strings.Repeat("c", 64))
|
||
c.Put(idA, make([]byte, 50))
|
||
c.Put(idB, make([]byte, 50))
|
||
// touch A so it's most-recently-used
|
||
if _, ok := c.Get(idA); !ok {
|
||
t.Fatal("A missing pre-promote")
|
||
}
|
||
c.Put(idC, make([]byte, 50)) // should evict B, not A
|
||
if _, ok := c.Get(idA); !ok {
|
||
t.Fatal("A should survive (was touched)")
|
||
}
|
||
if _, ok := c.Get(idB); ok {
|
||
t.Fatal("B should have been evicted")
|
||
}
|
||
}
|