Files
vfs/vfs_test.go
Hanzo DevandGitHub 6c3cfe9751 feat: cloud Mount() per HIP-0106 (#1)
* 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)
2026-05-18 23:37:45 -07:00

117 lines
3.0 KiB
Go

package vfs_test
import (
"context"
"strings"
"testing"
"github.com/luxfi/age"
"github.com/hanzoai/vfs/pkg/backend"
_ "github.com/hanzoai/vfs/pkg/backend/file"
"github.com/hanzoai/vfs"
)
func newRoundTripVFS(t *testing.T) *vfs.VFS {
t.Helper()
dir := t.TempDir()
be, err := backend.Open(context.Background(), "file://"+dir)
if err != nil {
t.Fatalf("backend.Open: %v", err)
}
t.Cleanup(func() { _ = be.Close() })
id, err := age.GenerateX25519Identity()
if err != nil {
t.Fatalf("age.GenerateX25519Identity: %v", err)
}
c, err := vfs.NewCrypto([]age.Recipient{id.Recipient()}, []age.Identity{id})
if err != nil {
t.Fatalf("NewCrypto: %v", err)
}
v, err := vfs.New(vfs.Config{
Backend: be,
Crypto: c,
CacheMax: 0, // unbounded for the test
})
if err != nil {
t.Fatalf("vfs.New: %v", err)
}
return v
}
func TestRoundTrip(t *testing.T) {
v := newRoundTripVFS(t)
ctx := context.Background()
plaintext := []byte("hello, vfs world — this is a small block")
id, err := v.PutBlock(ctx, plaintext)
if err != nil {
t.Fatalf("PutBlock: %v", err)
}
if !strings.HasPrefix(string(id), "") || len(id) != 64 {
t.Fatalf("BlockID looks wrong: %q (len %d, want 64)", id, len(id))
}
got, err := v.GetBlock(ctx, id)
if err != nil {
t.Fatalf("GetBlock: %v", err)
}
// Decrypted block is zero-padded to BlockSize. Logical content is at the start.
if len(got) != vfs.BlockSize {
t.Fatalf("GetBlock returned %d bytes, want BlockSize=%d", len(got), vfs.BlockSize)
}
if string(got[:len(plaintext)]) != string(plaintext) {
t.Fatalf("plaintext mismatch:\n got=%q\n want=%q", got[:len(plaintext)], plaintext)
}
for i := len(plaintext); i < vfs.BlockSize; i++ {
if got[i] != 0 {
t.Fatalf("byte %d should be zero pad, got 0x%x", i, got[i])
}
}
}
func TestRoundTripDeduplicates(t *testing.T) {
// Same plaintext + same recipient + same identity → identical ciphertext is NOT
// guaranteed by age (it includes a random ephemeral key). What we DO guarantee
// is that repeated PutBlock calls of the same plaintext return DIFFERENT block
// IDs but each is independently retrievable. (Cross-fleet dedup is a separate
// design with deterministic key derivation; that lands in 0.2.0+.)
v := newRoundTripVFS(t)
ctx := context.Background()
id1, err := v.PutBlock(ctx, []byte("identical"))
if err != nil {
t.Fatalf("PutBlock 1: %v", err)
}
id2, err := v.PutBlock(ctx, []byte("identical"))
if err != nil {
t.Fatalf("PutBlock 2: %v", err)
}
// Both must round-trip even if the IDs differ.
for _, id := range []vfs.BlockID{id1, id2} {
if _, err := v.GetBlock(ctx, id); err != nil {
t.Fatalf("GetBlock(%s): %v", id, err)
}
}
}
func TestStats(t *testing.T) {
v := newRoundTripVFS(t)
ctx := context.Background()
for i := 0; i < 3; i++ {
if _, err := v.PutBlock(ctx, []byte{byte(i)}); err != nil {
t.Fatalf("PutBlock: %v", err)
}
}
s := v.Stats()
if s.CacheBlocks != 3 {
t.Fatalf("CacheBlocks = %d, want 3", s.CacheBlocks)
}
if s.Backend == "" {
t.Fatal("Stats.Backend empty")
}
}