Files
seaweedfs/sw-block/cmd/v2singleblock/main_test.go
T
pingqiuandClaude Opus 4.6 b8c6944e3f feat: V2 MVP milestone — masterv2 + volumev2 + in-process failover
V2 runtime packages:
- sw-block/runtime/masterv2: identity authority (desired state,
  heartbeat handling, promotion arbitration via SelectPromotionCandidate)
- sw-block/runtime/volumev2: per-volume micro-cluster shell (node,
  orchestrator, control session, iSCSI frontend, takeover gate,
  failover session + driver, replica summary reconstruction)
- sw-block/runtime/purev2: RF1 execution shell (engine + store +
  dispatcher + local boundary observations)
- sw-block/runtime/protocolv2: three-channel separation
  (heartbeat/assignment/query + replica summary)

V2 binaries:
- sw-block/cmd/v2singleblock: single-node RF1 block server
- sw-block/cmd/purev2rf1: minimal RF1 runtime binary

Milestone capabilities:
- RF1 write/read/sync with engine-driven mode projection
- masterv2 ↔ volumev2 heartbeat convergence + assignment reissue
- Promotion query with fresh CommittedLSN/WALHeadLSN evidence
- Replica summary for bounded takeover reconstruction
- Primary-loss reconstruction from peer summaries (fail-closed gate)
- In-process failover driver with session observability
- Local boundary observations feed engine (Committed/Durable/Checkpoint)

Design docs:
- v2-two-loop-protocol.md: identity vs data-control separation
- v2-automata-ownership-map.md: event/command ownership split
- v2-loop1-surface-draft.md: heartbeat/query/assignment field spec
- v2-volumev2-single-node-mvp.md: target layering
- v2-kernel-closure-review.md: per-volume micro-cluster principle
- v2-pure-runtime-rf1-bootstrap.md, v2-capability-map.md,
  v2-proof-and-retest-pyramid.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:08:02 -07:00

130 lines
3.1 KiB
Go

package main
import (
"bytes"
"encoding/json"
"path/filepath"
"strings"
"testing"
)
func TestRunSmoke_ProducesSingleNodeMVPResult(t *testing.T) {
path := filepath.Join(t.TempDir(), "single-node-mvp.blk")
var out bytes.Buffer
oldOutput := commandOutput
commandOutput = &out
defer func() {
commandOutput = oldOutput
}()
runErr := runSmoke([]string{
"--path", path,
"--name", "mvp-vol",
"--node", "node-a",
"--write-text", "hello-v2",
})
if runErr != nil {
t.Fatalf("runSmoke: %v", runErr)
}
var result smokeResult
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
t.Fatalf("unmarshal output %q: %v", out.String(), err)
}
if result.VolumeName != "mvp-vol" {
t.Fatalf("volume_name=%q", result.VolumeName)
}
if result.Role != "primary" {
t.Fatalf("role=%q", result.Role)
}
if result.Mode != "allocated_only" {
t.Fatalf("mode=%q", result.Mode)
}
if !strings.Contains(result.Readback, "68656c6c6f2d7632") {
t.Fatalf("readback_hex=%q", result.Readback)
}
}
func TestRunRestartSmoke_PreservesDataAcrossRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "single-node-restart.blk")
var out bytes.Buffer
oldOutput := commandOutput
commandOutput = &out
defer func() {
commandOutput = oldOutput
}()
runErr := runRestartSmoke([]string{
"--path", path,
"--name", "restart-vol",
"--node", "node-a",
"--write-text", "hello-restart",
})
if runErr != nil {
t.Fatalf("runRestartSmoke: %v", runErr)
}
var result restartSmokeResult
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
t.Fatalf("unmarshal output %q: %v", out.String(), err)
}
if result.VolumeName != "restart-vol" {
t.Fatalf("volume_name=%q", result.VolumeName)
}
if result.Role != "primary" {
t.Fatalf("role=%q", result.Role)
}
if result.Mode != "allocated_only" {
t.Fatalf("mode=%q", result.Mode)
}
if result.InitialWrite != result.PostRestart {
t.Fatalf("initial=%q post_restart=%q", result.InitialWrite, result.PostRestart)
}
if !strings.Contains(result.PostRestart, "68656c6c6f2d72657374617274") {
t.Fatalf("post_restart_hex=%q", result.PostRestart)
}
}
func TestRunISCSISmoke_ExportsFrontendAndReportsAddress(t *testing.T) {
path := filepath.Join(t.TempDir(), "single-node-iscsi.blk")
var out bytes.Buffer
oldOutput := commandOutput
commandOutput = &out
defer func() {
commandOutput = oldOutput
}()
runErr := runISCSISmoke([]string{
"--path", path,
"--name", "iscsi-vol",
"--node", "node-a",
"--iqn", "iqn.2026-04.com.seaweedfs:test.cli",
})
if runErr != nil {
t.Fatalf("runISCSISmoke: %v", runErr)
}
var result iscsiSmokeResult
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
t.Fatalf("unmarshal output %q: %v", out.String(), err)
}
if result.VolumeName != "iscsi-vol" {
t.Fatalf("volume_name=%q", result.VolumeName)
}
if result.Role != "primary" {
t.Fatalf("role=%q", result.Role)
}
if result.Mode != "allocated_only" {
t.Fatalf("mode=%q", result.Mode)
}
if result.IQN != "iqn.2026-04.com.seaweedfs:test.cli" {
t.Fatalf("iqn=%q", result.IQN)
}
if !strings.Contains(result.Address, "127.0.0.1:") {
t.Fatalf("address=%q", result.Address)
}
}