package storage import ( "context" "fmt" "path/filepath" "testing" "github.com/metazla/meta-core/internal/claim" "github.com/metazla/meta-core/internal/identity" "github.com/metazla/meta-core/internal/kv" ) // openClient opens (and starts) a client on dir's truth + cache files. func openClient(t *testing.T, dir string) *Client { t.Helper() c := NewClient() if err := c.Connect(filepath.Join(dir, "meta-core.db"), filepath.Join(dir, "cache.db")); err != nil { t.Fatalf("connect: %v", err) } if err := c.EnsureSchemaVersion(SchemaVersion); err != nil { t.Fatalf("schema: %v", err) } if err := c.Start(); err != nil { t.Fatalf("start: %v", err) } return c } // newTestClient opens a fresh store in the test's temp dir. func newTestClient(t *testing.T) *Client { t.Helper() c := openClient(t, t.TempDir()) t.Cleanup(func() { _ = c.Close() }) return c } // signer returns a deterministic test identity (scalar n repeated). func signer(t *testing.T, n byte) *identity.Identity { t.Helper() id, err := identity.Import(fmt.Sprintf("%02x%062x", n, 1)) if err != nil { t.Fatalf("identity: %v", err) } return id } // vote builds a signed claim; v is a string (text), []byte (ciphertext) or nil // (retraction). func vote(t *testing.T, id *identity.Identity, cid, key string, v any, ts uint64) claim.Claim { t.Helper() cl := claim.Claim{CID: cid, UID: id.UID, Key: key, TS: ts} switch x := v.(type) { case string: cl.Value = claim.Text(x) case []byte: cl.Value = claim.Bytes(x) case nil: default: t.Fatalf("vote value %T", v) } if err := cl.Sign(id); err != nil { t.Fatalf("sign: %v", err) } return cl } // write posts claims and returns their statuses. func write(t *testing.T, c *Client, claims ...claim.Claim) []ClaimStatus { t.Helper() res, err := c.WriteClaims(context.Background(), claims) if err != nil { t.Fatalf("write: %v", err) } out := make([]ClaimStatus, len(res)) for i, r := range res { out[i] = r.Status } return out } // mustAccept posts claims and fails unless every one is accepted. func mustAccept(t *testing.T, c *Client, claims ...claim.Claim) { t.Helper() for i, st := range write(t, c, claims...) { if st != StatusAccepted { t.Fatalf("claim %d (%s/%s): %s", i, claims[i].CID, claims[i].Key, st) } } } // seed writes merged rows straight into the cache — the derived layout search // and the counters work on — bypassing claims. Search tests pin SQL behaviour // over that layout; how rows get there is the apply loop's business. func seed(t *testing.T, c *Client, records map[string]map[string]string) { t.Helper() for id, fields := range records { setMerged(t, c, id, fields) } } func setMerged(t *testing.T, c *Client, rid string, fields map[string]string) { t.Helper() if err := c.update(func(tx *kv.Tx) error { for k, v := range fields { if err := tx.Put(recordPrefix(rid)+k, v); err != nil { return err } } if err := tx.Put(rootKey(rid), ""); err != nil { return err } return refreshSearchTx(tx, rid) }); err != nil { t.Fatalf("seed %s: %v", rid, err) } } func deleteMerged(t *testing.T, c *Client, rid string) { t.Helper() if err := c.update(func(tx *kv.Tx) error { if _, err := tx.DeletePrefix(recordPrefix(rid), 0); err != nil { return err } if _, err := tx.Delete(rootKey(rid)); err != nil { return err } return deleteSearchRowTx(tx, rid) }); err != nil { t.Fatalf("delete %s: %v", rid, err) } } // allKeys lists every cache kv row (full scan). func allKeys(t *testing.T, c *Client) []string { t.Helper() var keys []string if err := c.Store().ScanPrefix(context.Background(), "", 1000, func(rows []kv.KV) error { for _, r := range rows { keys = append(keys, r.K) } return nil }); err != nil { t.Fatalf("scan: %v", err) } return keys } // rid resolves a cid to its record (fails when unknown). func rid(t *testing.T, c *Client, cid string) string { t.Helper() r, err := c.GetByCID(cid) if err != nil || r == "" { t.Fatalf("cid %s unresolved (%v)", cid, err) } return r } func record(t *testing.T, c *Client, cid string) map[string]string { t.Helper() f, err := c.GetMetadataFlat(rid(t, c, cid)) if err != nil { t.Fatal(err) } return f }