package protocol import ( "encoding/json" "testing" "time" ) func TestEnvelopeValidate_AgentHello(t *testing.T) { payload := AgentHelloPayload{ NodeID: "node-01", AgentVersion: "0.1.0", ProtocolVersion: CurrentProtocolVersion, } raw, err := json.Marshal(payload) if err != nil { t.Fatal(err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-001", Type: TypeAgentHello, Timestamp: time.Now().UTC(), Payload: raw, } if err := env.Validate(); err != nil { t.Fatalf("expected valid envelope, got %v", err) } if !env.Type.IsAgentToControl() { t.Fatal("agent.hello should be agent-to-control") } } func TestEnvelopeValidate_ServerStart(t *testing.T) { payload := ServerStartPayload{ OperationMeta: OperationMeta{ ServerID: "srv-01", Generation: 4, }, DesiredGeneration: 4, } raw, err := json.Marshal(payload) if err != nil { t.Fatal(err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-002", CorrelationID: "corr-002", Type: TypeServerStart, Timestamp: time.Now().UTC(), Payload: raw, } if err := env.Validate(); err != nil { t.Fatalf("expected valid envelope, got %v", err) } if !env.Type.IsControlToAgent() { t.Fatal("server.start should be control-to-agent") } } func TestEnvelopeValidate_RejectsInvalid(t *testing.T) { tests := []struct { name string env Envelope }{ { name: "missing message id", env: Envelope{ ProtocolVersion: 1, Type: TypeAgentHeartbeat, Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{"nodeId":"n1"}`), }, }, { name: "unknown type", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: "agent.unknown", Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{}`), }, }, { name: "non-utc timestamp", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: TypeAgentHeartbeat, Timestamp: time.Now().Local(), Payload: json.RawMessage(`{"nodeId":"n1"}`), }, }, { name: "invalid payload json", env: Envelope{ ProtocolVersion: 1, MessageID: "m1", Type: TypeAgentHeartbeat, Timestamp: time.Now().UTC(), Payload: json.RawMessage(`{not-json`), }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if err := tc.env.Validate(); err == nil { t.Fatal("expected validation error") } }) } } func TestDecodeEnvelope(t *testing.T) { data := []byte(`{ "protocolVersion": 1, "messageId": "01HXYZ", "type": "agent.heartbeat", "timestamp": "2026-01-01T00:00:00Z", "payload": {"nodeId":"node-01","runningServers":0} }`) env, err := DecodeEnvelope(data) if err != nil { t.Fatalf("decode failed: %v", err) } if env.Type != TypeAgentHeartbeat { t.Fatalf("unexpected type %s", env.Type) } } func TestServerProvisionPayload_JSONRoundtrip(t *testing.T) { deadline := time.Date(2026, 6, 26, 12, 0, 0, 0, time.UTC) original := ServerProvisionPayload{ OperationMeta: OperationMeta{ ServerID: "srv-abc123", Generation: 2, Deadline: &deadline, IdempotencyKey: "idem-001", }, Edition: "JAVA", SoftwareFamily: "PAPER", MinecraftVersion: "1.21.1", HostPort: 25565, RamMb: 2048, DataPath: "/var/lib/hexahost-gamecloud/srv-abc123", EulaAccepted: true, } data, err := json.Marshal(original) if err != nil { t.Fatalf("marshal: %v", err) } var decoded ServerProvisionPayload if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("unmarshal: %v", err) } if decoded.ServerID != original.ServerID { t.Fatalf("serverId: got %q want %q", decoded.ServerID, original.ServerID) } if decoded.Generation != original.Generation { t.Fatalf("generation: got %d want %d", decoded.Generation, original.Generation) } if decoded.Edition != original.Edition { t.Fatalf("edition: got %q want %q", decoded.Edition, original.Edition) } if decoded.SoftwareFamily != original.SoftwareFamily { t.Fatalf("softwareFamily: got %q want %q", decoded.SoftwareFamily, original.SoftwareFamily) } if decoded.MinecraftVersion != original.MinecraftVersion { t.Fatalf("minecraftVersion: got %q want %q", decoded.MinecraftVersion, original.MinecraftVersion) } if decoded.HostPort != original.HostPort { t.Fatalf("hostPort: got %d want %d", decoded.HostPort, original.HostPort) } if decoded.RamMb != original.RamMb { t.Fatalf("ramMb: got %d want %d", decoded.RamMb, original.RamMb) } if decoded.DataPath != original.DataPath { t.Fatalf("dataPath: got %q want %q", decoded.DataPath, original.DataPath) } if decoded.EulaAccepted != original.EulaAccepted { t.Fatalf("eulaAccepted: got %v want %v", decoded.EulaAccepted, original.EulaAccepted) } if decoded.IdempotencyKey != original.IdempotencyKey { t.Fatalf("idempotencyKey: got %q want %q", decoded.IdempotencyKey, original.IdempotencyKey) } if decoded.Deadline == nil || !decoded.Deadline.Equal(deadline) { t.Fatalf("deadline mismatch: got %v want %v", decoded.Deadline, deadline) } } func TestNewEnvelope(t *testing.T) { env, err := NewEnvelope(TypeAgentHello, "msg-new", AgentHelloPayload{ NodeID: "n1", AgentVersion: "0.1.0", ProtocolVersion: 1, }) if err != nil { t.Fatalf("new envelope: %v", err) } if env.ProtocolVersion != CurrentProtocolVersion { t.Fatalf("protocol version %d", env.ProtocolVersion) } } func TestPhase3Payloads_JSONRoundtrip(t *testing.T) { meta := OperationMeta{ ServerID: "srv-01", Generation: 3, } command := ServerCommandPayload{ OperationMeta: meta, Command: "say hello", } commandRaw, err := json.Marshal(command) if err != nil { t.Fatal(err) } var decodedCommand ServerCommandPayload if err := json.Unmarshal(commandRaw, &decodedCommand); err != nil { t.Fatal(err) } if decodedCommand.Command != command.Command { t.Fatalf("command: got %q want %q", decodedCommand.Command, command.Command) } logsSub := ServerLogsSubscribePayload{ OperationMeta: meta, TailLines: 100, Follow: true, } logsSubRaw, err := json.Marshal(logsSub) if err != nil { t.Fatal(err) } var decodedLogsSub ServerLogsSubscribePayload if err := json.Unmarshal(logsSubRaw, &decodedLogsSub); err != nil { t.Fatal(err) } if decodedLogsSub.TailLines != 100 || !decodedLogsSub.Follow { t.Fatalf("logs subscribe mismatch: %+v", decodedLogsSub) } filesList := ServerFilesListPayload{ OperationMeta: meta, Path: "world", } filesListRaw, err := json.Marshal(filesList) if err != nil { t.Fatal(err) } env := &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-files-list", Type: TypeServerFilesList, Timestamp: time.Now().UTC(), Payload: filesListRaw, } if err := env.Validate(); err != nil { t.Fatalf("files list envelope: %v", err) } filesRead := ServerFilesReadPayload{ OperationMeta: meta, Path: "server.properties", MaxBytes: 4096, } filesReadRaw, err := json.Marshal(filesRead) if err != nil { t.Fatal(err) } env = &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-files-read", Type: TypeServerFilesRead, Timestamp: time.Now().UTC(), Payload: filesReadRaw, } if err := env.Validate(); err != nil { t.Fatalf("files read envelope: %v", err) } filesWrite := ServerFilesWritePayload{ OperationMeta: meta, Path: "ops.txt", Content: "test", Create: true, } filesWriteRaw, err := json.Marshal(filesWrite) if err != nil { t.Fatal(err) } env = &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-files-write", Type: TypeServerFilesWrite, Timestamp: time.Now().UTC(), Payload: filesWriteRaw, } if err := env.Validate(); err != nil { t.Fatalf("files write envelope: %v", err) } logLine := ServerLogPayload{ OperationMeta: meta, Line: "[Server] Done", Stream: "stdout", } logLineRaw, err := json.Marshal(logLine) if err != nil { t.Fatal(err) } env = &Envelope{ ProtocolVersion: CurrentProtocolVersion, MessageID: "msg-log", Type: TypeServerLog, Timestamp: time.Now().UTC(), Payload: logLineRaw, } if err := env.Validate(); err != nil { t.Fatalf("server log envelope: %v", err) } } func TestServerOperationResultPayload_WithResult(t *testing.T) { listResult, err := json.Marshal(ServerFilesListResultPayload{ Entries: []FileEntry{{ Name: "server.properties", Path: "server.properties", IsDir: false, Size: 128, ModifiedAt: "2026-06-26T12:00:00Z", }}, }) if err != nil { t.Fatal(err) } payload := ServerOperationResultPayload{ OperationMeta: OperationMeta{ServerID: "srv-01", Generation: 1}, ResultCode: "ok", Operation: "files.list", Result: listResult, } raw, err := json.Marshal(payload) if err != nil { t.Fatal(err) } var decoded ServerOperationResultPayload if err := json.Unmarshal(raw, &decoded); err != nil { t.Fatal(err) } if decoded.Operation != "files.list" { t.Fatalf("operation: got %q", decoded.Operation) } if len(decoded.Result) == 0 { t.Fatal("expected result payload") } var result ServerFilesListResultPayload if err := json.Unmarshal(decoded.Result, &result); err != nil { t.Fatal(err) } if len(result.Entries) != 1 || result.Entries[0].Name != "server.properties" { t.Fatalf("unexpected list result: %+v", result) } }